Apache Thrift系列詳解(三) - 序列化機(jī)制

前言

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)型

  1. 基本類(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)制串
  2. 結(jié)構(gòu)體類(lèi)型:
      struct: 定義的結(jié)構(gòu)體對(duì)象
  3. 容器類(lèi)型:
      list: 有序元素列表
      set: 無(wú)序無(wú)重復(fù)元素集合
      map: 有序的key/value集合
  4. 異常類(lèi)型:
      exception: 異常類(lèi)型
  5. 服務(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ò)程正常完成。

image

Thrift協(xié)議源碼

(一) writeData()分析

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

image

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

image

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

image

這里完成了幾步操作:

(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)記,包括typefield-id。type是字段的數(shù)據(jù)類(lèi)型的標(biāo)識(shí)號(hào),field-idThrift 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)度為8byte字節(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)

image

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

image

這里完成的幾步操作:

(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ù)棧

零壹技術(shù)棧

本帳號(hào)將持續(xù)分享后端技術(shù)干貨,包括虛擬機(jī)基礎(chǔ),多線(xiàn)程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務(wù),架構(gòu)學(xué)習(xí)和進(jìn)階等學(xué)習(xí)資料和文章。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容