Thrift
支持二進制,壓縮格式,以及json
格式數據的序列化和反序列化。開發人員能夠更加靈活的選擇協議的具體形式。協議是可自由擴展的,新版本的協議,徹底兼容老的版本!java
當前流行的數據交換格式能夠分爲以下幾類:apache
序列化的數據包含完整的結構, 包含了field
名稱和value
值。好比xml/json/java serizable
,大百度的mcpack/compack
,都屬於此類。即調整不一樣屬性的順序對序列化/反序列化不形成影響。編程
序列化的數據,丟棄了部分信息, 好比field
名稱, 但引入了index
(經常是id
+type
的方式)來對應具體屬性和值。這方面的表明有google protobuf/thrift
也屬於此類。json
傳說中大百度的infpack
實現,就是藉助該種方式來實現,丟棄了不少有效信息,性能/壓縮比最好,不過向後兼容須要開發作必定的工做, 詳情不知。後端
交換格式 | 類型 | 優勢 | 缺點 |
---|---|---|---|
Xml | 文本 | 易讀 | 臃腫,不支持二進制數據類型 |
JSON | 文本 | 易讀 | 丟棄了類型信息,好比"score":100,對score類型是int/double解析有二義性, 不支持二進制數據類型 |
Java serizable | 二進制 | 使用簡單 | 臃腫,只限制在JAVA領域 |
Thrift | 二進制 | 高效 | 不易讀,向後兼容有必定的約定限制 |
Google Protobuf | 二進制 | 高效 | 不易讀,向後兼容有必定的約定限制 |
Thrift
可讓用戶選擇客戶端與服務端之間傳輸通訊協議的類別,在傳輸協議上整體劃分爲文本(text
)和二進制(binary
)傳輸協議。爲節約帶寬,提升傳輸效率,通常狀況下使用二進制類型的傳輸協議爲多數,有時還會使用基於文本類型的協議,這須要根據項目/產品中的實際需求。經常使用協議有如下幾種:數組
JSON
文本的數據編碼協議進行數據傳輸JSON
只寫的協議,適用於經過腳本語言解析(a). 首先編寫一個簡單的thrift
文件pair.thrift
:緩存
struct Pair {
1: required string key
2: required string value
}
複製代碼
這裏標識了
required
的字段,要求在使用時必須正確賦值,不然運行時會拋出TProtocolException
異常。缺省和指定爲optional
時,則運行時不作字段非空校驗。bash
(b). 編譯並生成java
源代碼:網絡
thrift -gen java pair.thrift
複製代碼
(c). 編寫序列化和反序列化的測試代碼:多線程
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
的序列化機制,即數據寫入實現,這裏採用二進制協議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) {}
複製代碼
(c). 逐一寫入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這五個字母來分別表示十、十一、十二、1三、1四、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)。
public void writeByte(byte b) throws TException {
inoutTemp[0] = b;
trans_.write(inoutTemp, 0, 1);
}
複製代碼
public void writeBool(boolean b) throws TException {
writeByte(b ? (byte)1 : (byte)0);
}
複製代碼
short
類型數據。public void writeI16(short i16) throws TException {
inoutTemp[0] = (byte)(0xff & (i16 >> 8));
inoutTemp[1] = (byte)(0xff & (i16));
trans_.write(inoutTemp, 0, 2);
}
複製代碼
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);
}
複製代碼
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);
}
複製代碼
double
類型數據。public void writeDouble(double dub) throws TException {
writeI64(Double.doubleToLongBits(dub));
}
複製代碼
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");
}
}
複製代碼
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() {}
複製代碼
查看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;
}
// 字段的讀取,省略...
}
複製代碼
(c). 根據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()
方法。
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];
}
複製代碼
public boolean readBool() throws TException {
return (readByte() == 1);
}
複製代碼
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)));
}
複製代碼
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[off+3] & 0xff));
}
複製代碼
long
類型數據。public long readI64() throws TException {
byte[] buf = inoutTemp;
int off = 0;
if (trans_.getBytesRemainingInBuffer() >= 8) {
buf = trans_.getBuffer();
off = trans_.getBufferPosition();
trans_.consumeBuffer(8);
} else {
readAll(inoutTemp, 0, 8);
}
return ((long)(buf[off] & 0xff) << 56) |
((long)(buf[off+1] & 0xff) << 48) |
((long)(buf[off+2] & 0xff) << 40) |
((long)(buf[off+3] & 0xff) << 32) |
((long)(buf[off+4] & 0xff) << 24) |
((long)(buf[off+5] & 0xff) << 16) |
((long)(buf[off+6] & 0xff) << 8) |
((long)(buf[off+7] & 0xff));
}
複製代碼
double
類型數據。public double readDouble() throws TException {
return Double.longBitsToDouble(readI64());
}
複製代碼
4
字節的字符串長度,而後檢查NIO
緩衝區中是否有對應長度的字節未消費。若是有,直接從緩衝區中讀取;不然,從傳輸通道中讀取數據。public String readString() throws TException {
int size = readI32();
checkStringReadLength(size);
if (trans_.getBytesRemainingInBuffer() >= size) {
try {
String s = new String(trans_.getBuffer(), trans_.getBufferPosition(), size, "UTF-8");
trans_.consumeBuffer(size);
return s;
} catch (UnsupportedEncodingException e) {
throw new TException("JVM DOES NOT SUPPORT UTF-8");
}
}
return readStringBody(size);
}
複製代碼
若是是從傳輸通道中讀取數據,查看readStringBody()
方法:
public String readStringBody(int size) throws TException {
try {
byte[] buf = new byte[size];
trans_.readAll(buf, 0, size);
return new String(buf, "UTF-8");
} catch (UnsupportedEncodingException uex) {
throw new TException("JVM DOES NOT SUPPORT UTF-8");
}
}
複製代碼
ByteBuffer
字節緩存對象。public ByteBuffer readBinary() throws TException {
int size = readI32();
checkStringReadLength(size);
if (trans_.getBytesRemainingInBuffer() >= size) {
ByteBuffer bb = ByteBuffer.wrap(trans_.getBuffer(), trans_.getBufferPosition(), size);
trans_.consumeBuffer(size);
return bb;
}
byte[] buf = new byte[size];
trans_.readAll(buf, 0, size);
return ByteBuffer.wrap(buf);
}
複製代碼
(d). 每一個字段數據讀取完成後,都須要再讀取一個字段結束標記。
public void readFieldEnd() {}
複製代碼
(e). 當全部字段讀取完成後,須要經過readStructEnd()
再讀入一個結構完成標記。
public void readStructEnd() {}
複製代碼
(f). 讀取結束後,一樣須要校驗在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());
}
}
複製代碼
其實到這裏,對於Thrift
的序列化機制和反序列化機制的具體實現和高效性,相信各位已經有了比較深刻的認識!
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。