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

前言

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的數據類型

  1. 基本類型:   bool: 布爾值   byte: 8位有符號整數   i16: 16位有符號整數   i32: 32位有符號整數   i64: 64位有符號整數   double: 64位浮點數   string: UTF-8編碼的字符串   binary: 二進制串
  2. 結構體類型:   struct: 定義的結構體對象
  3. 容器類型:   list: 有序元素列表   set: 無序無重複元素集合   map: 有序的key/value集合
  4. 異常類型:   exception: 異常類型
  5. 服務類型:   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時,則運行時不作字段非空校驗。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協議源碼

(一) 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). 首先是字段開始標記,包括typefield-idtype是字段的數據類型的標識號field-idThrift 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經過一個長度爲8byte字節數組緩存寫入讀取的臨時字節數據。

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

  • writeByte():寫入單個字節數據。
public void writeByte(byte b) throws TException {
  inoutTemp[0] = b;
  trans_.write(inoutTemp, 01);
}
複製代碼
  • 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, 02);
}
複製代碼
  • 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, 04);
}
複製代碼
  • 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, 08);
}
複製代碼
  • 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, 01);
  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, 02);
  }

  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, 04);
  }
  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, 08);
  }

  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序列化機制反序列化機制具體實現高效性,相信各位已經有了比較深刻的認識!

相關連接

  1. Apache Thrift系列詳解(一) - 概述與入門

  2. Apache Thrift系列詳解(二) - 網絡服務模型

  3. Apache Thrift系列詳解(三) - 序列化機制


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索