前言
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
復制代碼
(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協議源碼
(一) 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) {} 復制代碼
(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這五個字母來分別表示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; } // 字段的讀取,省略... } 復制代碼
(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()
方法。
- 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[off+3] & 0xff)); } 復制代碼
- readI64():讀取長整型
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)); } 復制代碼
- readDouble():讀取雙精度浮點
double
類型數據。
public double readDouble() throws TException { return Double.longBitsToDouble(readI64()); } 復制代碼
- readString():讀取字符串類型的數據,首先讀取並校驗
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"); } } 復制代碼
- readBinary():讀取二進制數組類型數據,和字符串讀取類似,返回一個
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
的序列化機制和反序列化機制的具體實現和高效性,相信各位已經有了比較深入的認識!