前言
Thrift支持二進(jìn)制,壓縮格式,以及json格式數(shù)據(jù)的序列化和反序列化。開(kāi)發(fā)人員可以更加靈活的選擇協(xié)議的具體形式。協(xié)議是可自由擴(kuò)展的,新版本的協(xié)議,完全兼容老的版本!
正文
數(shù)據(jù)交換格式簡(jiǎn)介
當(dāng)前流行的數(shù)據(jù)交換格式可以分為如下幾類(lèi):
(一) 自解析型
序列化的數(shù)據(jù)包含完整的結(jié)構(gòu), 包含了field名稱(chēng)和value值。比如xml/json/java serizable,大百度的mcpack/compack,都屬于此類(lèi)。即調(diào)整不同屬性的順序對(duì)序列化/反序列化不造成影響。
(二) 半解析型
序列化的數(shù)據(jù),丟棄了部分信息, 比如field名稱(chēng), 但引入了index(常常是id+type的方式)來(lái)對(duì)應(yīng)具體屬性和值。這方面的代表有google protobuf/thrift也屬于此類(lèi)。
(三) 無(wú)解析型
傳說(shuō)中大百度的infpack實(shí)現(xiàn),就是借助該種方式來(lái)實(shí)現(xiàn),丟棄了很多有效信息,性能/壓縮比最好,不過(guò)向后兼容需要開(kāi)發(fā)做一定的工作, 詳情不知。
| 交換格式 | 類(lèi)型 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|---|
| Xml | 文本 | 易讀 | 臃腫,不支持二進(jìn)制數(shù)據(jù)類(lèi)型 |
| JSON | 文本 | 易讀 | 丟棄了類(lèi)型信息,比如"score":100,對(duì)score類(lèi)型是int/double解析有二義性, 不支持二進(jìn)制數(shù)據(jù)類(lèi)型 |
| Java serizable | 二進(jìn)制 | 使用簡(jiǎn)單 | 臃腫,只限制在JAVA領(lǐng)域 |
| Thrift | 二進(jìn)制 | 高效 | 不易讀,向后兼容有一定的約定限制 |
| Google Protobuf | 二進(jìn)制 | 高效 | 不易讀,向后兼容有一定的約定限制 |
Thrift的數(shù)據(jù)類(lèi)型
-
基本類(lèi)型:
bool: 布爾值
byte: 8位有符號(hào)整數(shù)
i16: 16位有符號(hào)整數(shù)
i32: 32位有符號(hào)整數(shù)
i64: 64位有符號(hào)整數(shù)
double: 64位浮點(diǎn)數(shù)
string: UTF-8編碼的字符串
binary: 二進(jìn)制串 -
結(jié)構(gòu)體類(lèi)型:
struct: 定義的結(jié)構(gòu)體對(duì)象 -
容器類(lèi)型:
list: 有序元素列表
set: 無(wú)序無(wú)重復(fù)元素集合
map: 有序的key/value集合 -
異常類(lèi)型:
exception: 異常類(lèi)型 -
服務(wù)類(lèi)型:
service: 具體對(duì)應(yīng)服務(wù)的類(lèi)
Thrift的序列化協(xié)議
Thrift可以讓用戶(hù)選擇客戶(hù)端與服務(wù)端之間傳輸通信協(xié)議的類(lèi)別,在傳輸協(xié)議上總體劃分為文本(text)和二進(jìn)制(binary)傳輸協(xié)議。為節(jié)約帶寬,提高傳輸效率,一般情況下使用二進(jìn)制類(lèi)型的傳輸協(xié)議為多數(shù),有時(shí)還會(huì)使用基于文本類(lèi)型的協(xié)議,這需要根據(jù)項(xiàng)目/產(chǎn)品中的實(shí)際需求。常用協(xié)議有以下幾種:
- TBinaryProtocol:二進(jìn)制編碼格式進(jìn)行數(shù)據(jù)傳輸
- TCompactProtocol:高效率的、密集的二進(jìn)制編碼格式進(jìn)行數(shù)據(jù)傳輸
- TJSONProtocol: 使用
JSON文本的數(shù)據(jù)編碼協(xié)議進(jìn)行數(shù)據(jù)傳輸 - TSimpleJSONProtocol:只提供
JSON只寫(xiě)的協(xié)議,適用于通過(guò)腳本語(yǔ)言解析
Thrift的序列化測(cè)試
(a). 首先編寫(xiě)一個(gè)簡(jiǎn)單的thrift文件pair.thrift:
struct Pair {
1: required string key
2: required string value
}
這里標(biāo)識(shí)了
required的字段,要求在使用時(shí)必須正確賦值,否則運(yùn)行時(shí)會(huì)拋出TProtocolException異常。缺省和指定為optional時(shí),則運(yùn)行時(shí)不做字段非空校驗(yàn)。
(b). 編譯并生成java源代碼:
thrift -gen java pair.thrift
(c). 編寫(xiě)序列化和反序列化的測(cè)試代碼:
- 序列化測(cè)試,將
Pair對(duì)象寫(xiě)入文件中
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();
}
- 反序列化測(cè)試,從文件中解析生成
Pair對(duì)象
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) 觀(guān)察運(yùn)行結(jié)果,正常輸出表明序列化和反序列化過(guò)程正常完成。

Thrift協(xié)議源碼
(一) writeData()分析
首先查看thrift的序列化機(jī)制,即數(shù)據(jù)寫(xiě)入實(shí)現(xiàn),這里采用二進(jìn)制協(xié)議TBinaryProtocol,切入點(diǎn)為pair.write(TProtocol):

查看scheme()方法,決定采用元組計(jì)劃(TupleScheme)還是標(biāo)準(zhǔn)計(jì)劃(StandardScheme)來(lái)實(shí)現(xiàn)序列化,默認(rèn)采用的是標(biāo)準(zhǔn)計(jì)劃StandardScheme。

標(biāo)準(zhǔn)計(jì)劃(StandardScheme)下的write()方法:

這里完成了幾步操作:
(a). 根據(jù)Thrift IDL文件中定義了required的字段驗(yàn)證字段是否正確賦值。
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). 通過(guò)writeStructBegin()記錄寫(xiě)入結(jié)構(gòu)的開(kāi)始標(biāo)記。
public void writeStructBegin(TStruct struct) {}
(c). 逐一寫(xiě)入Pair對(duì)象的各個(gè)字段,包括字段字段開(kāi)始標(biāo)記、字段的值和字段結(jié)束標(biāo)記。
if (struct.key != null) {
oprot.writeFieldBegin(KEY_FIELD_DESC);
oprot.writeString(struct.key);
oprot.writeFieldEnd();
}
// 省略...
(1). 首先是字段開(kāi)始標(biāo)記,包括type和field-id。type是字段的數(shù)據(jù)類(lèi)型的標(biāo)識(shí)號(hào),field-id是Thrift IDL定義的字段次序,比如說(shuō)key為1,value為2。
public void writeFieldBegin(TField field) throws TException {
writeByte(field.type);
writeI16(field.id);
}
Thrift提供了TType,對(duì)不同的數(shù)據(jù)類(lèi)型(type)提供了唯一標(biāo)識(shí)的typeID。
public final class TType {
public static final byte STOP = 0; // 數(shù)據(jù)讀寫(xiě)完成
public static final byte VOID = 1; // 空值
public static final byte BOOL = 2; // 布爾值
public static final byte BYTE = 3; // 字節(jié)
public static final byte DOUBLE = 4; // 雙精度浮點(diǎn)型
public static final byte I16 = 6; // 短整型
public static final byte I32 = 8; // 整型
public static final byte I64 = 10; // 長(zhǎng)整型
public static final byte STRING = 11; // 字符串類(lèi)型
public static final byte STRUCT = 12; // 引用類(lèi)型
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). 然后是寫(xiě)入字段的值,根據(jù)字段的數(shù)據(jù)類(lèi)型又歸納為以下實(shí)現(xiàn):writeByte()、writeBool()、writeI32()、writeI64()、writeDouble()、writeString()和writeBinary()方法。
TBinaryProtocol通過(guò)一個(gè)長(zhǎng)度為8的byte字節(jié)數(shù)組緩存寫(xiě)入或讀取的臨時(shí)字節(jié)數(shù)據(jù)。
private final byte[] inoutTemp = new byte[8];
常識(shí)1:16進(jìn)制的介紹。以0x開(kāi)始的數(shù)據(jù)表示16進(jìn)制,0xff換成十進(jìn)制為255。在16進(jìn)制中,A、B、C、D、E、F這五個(gè)字母來(lái)分別表示10、11、12、13、14、15。
16進(jìn)制變十進(jìn)制:f表示15。第n位的權(quán)值為16的n次方,由右到左從0位起:0xff = 1516^1 + 1516^0 = 255
16進(jìn)制變二進(jìn)制再變十進(jìn)制:0xff = 1111 1111 = 2^8 - 1 = 255
常識(shí)2:位運(yùn)算符的使用。>>表示代表右移符號(hào),如:int i=15; i>>2的結(jié)果是3,移出的部分將被拋棄。而<<表示左移符號(hào),與>>剛好相反。
轉(zhuǎn)為二進(jìn)制的形式可能更好理解,0000 1111(15)右移2位的結(jié)果是0000 0011(3),0001 1010(18)右移3位的結(jié)果是0000 0011(3)。
- writeByte():寫(xiě)入單個(gè)字節(jié)數(shù)據(jù)。
public void writeByte(byte b) throws TException {
inoutTemp[0] = b;
trans_.write(inoutTemp, 0, 1);
}
- writeBool():寫(xiě)入布爾值數(shù)據(jù)。
public void writeBool(boolean b) throws TException {
writeByte(b ? (byte)1 : (byte)0);
}
-
writeI16():寫(xiě)入短整型
short類(lèi)型數(shù)據(jù)。
public void writeI16(short i16) throws TException {
inoutTemp[0] = (byte)(0xff & (i16 >> 8));
inoutTemp[1] = (byte)(0xff & (i16));
trans_.write(inoutTemp, 0, 2);
}
-
writeI32():寫(xiě)入整型
int類(lèi)型數(shù)據(jù)。
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():寫(xiě)入長(zhǎng)整型
long類(lèi)型數(shù)據(jù)。
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():寫(xiě)入雙浮點(diǎn)型
double類(lèi)型數(shù)據(jù)。
public void writeDouble(double dub) throws TException {
writeI64(Double.doubleToLongBits(dub));
}
- writeString():寫(xiě)入字符串類(lèi)型,這里先寫(xiě)入字符串長(zhǎng)度,再寫(xiě)入字符串內(nèi)容。
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:寫(xiě)入二進(jìn)制數(shù)組類(lèi)型數(shù)據(jù),這里數(shù)據(jù)輸入是
NIO中的ByteBuffer類(lèi)型。
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). 每個(gè)字段寫(xiě)入完成后,都需要記錄字段結(jié)束標(biāo)記。
public void writeFieldEnd() {}
(d). 當(dāng)所有的字段都寫(xiě)入以后,需要記錄字段停止標(biāo)記。
public void writeFieldStop() throws TException {
writeByte(TType.STOP);
}
(e). 當(dāng)所有數(shù)據(jù)寫(xiě)入完成后,通過(guò)writeStructEnd()記錄寫(xiě)入結(jié)構(gòu)的完成標(biāo)記。
public void writeStructEnd() {}
(二) readData()分析
查看thrift的反序列化機(jī)制,即數(shù)據(jù)讀取實(shí)現(xiàn),同樣采用二進(jìn)制協(xié)議TBinaryProtocol,切入點(diǎn)為pair.read(TProtocol):

數(shù)據(jù)讀取和數(shù)據(jù)寫(xiě)入一樣,也是采用的標(biāo)準(zhǔn)計(jì)劃StandardScheme。標(biāo)準(zhǔn)計(jì)劃(StandardScheme)下的read()方法:

這里完成的幾步操作:
(a). 通過(guò)readStructBegin讀取結(jié)構(gòu)的開(kāi)始標(biāo)記。
iprot.readStructBegin();
(b). 循環(huán)讀取結(jié)構(gòu)中的所有字段數(shù)據(jù)到Pair對(duì)象中,直到讀取到org.apache.thrift.protocol.TType.STOP為止。iprot.readFieldBegin()指明開(kāi)始讀取下一個(gè)字段的前需要讀取字段開(kāi)始標(biāo)記。
while (true) {
schemeField = iprot.readFieldBegin();
if (schemeField.type == org.apache.thrift.protocol.TType.STOP) {
break;
}
// 字段的讀取,省略...
}
(c). 根據(jù)Thrift IDL定義的field-id讀取對(duì)應(yīng)的字段,并賦值到Pair對(duì)象中,并設(shè)置Pair對(duì)象相應(yīng)的字段為已讀狀態(tài)(前提:字段在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);
}
關(guān)于讀取字段的值,根據(jù)字段的數(shù)據(jù)類(lèi)型也分為以下實(shí)現(xiàn):readByte()、readBool()、readI32()、readI64()、readDouble()、readString()和readBinary()方法。
- readByte():讀取單個(gè)字節(jié)數(shù)據(jù)。
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():讀取布爾值數(shù)據(jù)。
public boolean readBool() throws TException {
return (readByte() == 1);
}
-
readI16():讀取短整型
short類(lèi)型數(shù)據(jù)。
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類(lèi)型數(shù)據(jù)。
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():讀取長(zhǎng)整型
long類(lèi)型數(shù)據(jù)。
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():讀取雙精度浮點(diǎn)
double類(lèi)型數(shù)據(jù)。
public double readDouble() throws TException {
return Double.longBitsToDouble(readI64());
}
-
readString():讀取字符串類(lèi)型的數(shù)據(jù),首先讀取并校驗(yàn)
4字節(jié)的字符串長(zhǎng)度,然后檢查NIO緩沖區(qū)中是否有對(duì)應(yīng)長(zhǎng)度的字節(jié)未消費(fèi)。如果有,直接從緩沖區(qū)中讀?。环駝t,從傳輸通道中讀取數(shù)據(jù)。
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);
}
如果是從傳輸通道中讀取數(shù)據(jù),查看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():讀取二進(jìn)制數(shù)組類(lèi)型數(shù)據(jù),和字符串讀取類(lèi)似,返回一個(gè)
ByteBuffer字節(jié)緩存對(duì)象。
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). 每個(gè)字段數(shù)據(jù)讀取完成后,都需要再讀取一個(gè)字段結(jié)束標(biāo)記。
public void readFieldEnd() {}
(e). 當(dāng)所有字段讀取完成后,需要通過(guò)readStructEnd()再讀入一個(gè)結(jié)構(gòu)完成標(biāo)記。
public void readStructEnd() {}
(f). 讀取結(jié)束后,同樣需要校驗(yàn)在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());
}
}
總結(jié)
其實(shí)到這里,對(duì)于Thrift的序列化機(jī)制和反序列化機(jī)制的具體實(shí)現(xiàn)和高效性,相信各位已經(jīng)有了比較深入的認(rèn)識(shí)!
歡迎關(guān)注技術(shù)公眾號(hào): 零壹技術(shù)棧
本帳號(hào)將持續(xù)分享后端技術(shù)干貨,包括虛擬機(jī)基礎(chǔ),多線(xiàn)程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務(wù),架構(gòu)學(xué)習(xí)和進(jìn)階等學(xué)習(xí)資料和文章。