Thrift之TProtocol系列TCompactProtocol解析

TCompactProtocol协议作为TBinaryProtocol协议的升级强化版,都作为二进制编码传输方式,采用了一种乐器MIDI文件的编码方法(wiki,百度下),简单介绍下两种思想:

1: ZigZag有符号数编码,如表格所示:

  编码前 编码后
0 0
-1 1
1 2
-2 3
2 4
-3 5

其效果等效于正数等于原先 * 2,负数变正数。

32bits int =  (i << 1) ^ (i >> 31), 64bits long = (l << 1) ^ (l >> 63)

2:VLQ(variable-length quantity)编码:

           即一字节的最高位(MHB)为标志位,不参与具体的内容,意思数值的大小仅仅有其它七位来表示。当最高位bit为1时,表示下一个byte也是该数值的内容(下一个byte的低七位bits);当最高位bit为0时,下一个byte不参与其中。通过这样的方式,而不是int固定的4个bytes,long 8个bytes来讲,对于小数,能节约不少的空间大小;但凡事有利有弊,当数值比较大时,就要占用更多的空间,例如较大的int ,需要5bytes,较大的long需要10bytes.

   两者的结合 :

当VLQ编码遇到负数时,例如:long -1; 0XFFFFFFFFFFFFFFFF,就需要10bytes了,通过和ZigZag的结合,吧负数转变相应的正数。当正数,负数的 |数值|较小时,都可以通过两者的结合,有效的压缩占用的空间大小。但同上,数值较大不可避免的占用比平常正常编码更多的空间。

源码分析:

首先来看一下int32,long64的ZigZag编码:

  private long longToZigzag(long l) {
    return (l << 1) ^ (l >> 63);
  }

  /**
   * Convert n into a zigzag int. This allows negative numbers to be
   * represented compactly as a varint.
   */
  private int intToZigZag(int n) {
    return (n << 1) ^ (n >> 31);//正数 n << 1 扩大两倍 , n >> 31 = 0 , ^ 0 不变 ,2 * n ;
  }

再看看int32,long64的varint写法:

 byte[] i32buf = new byte[5]; //int32 最大需要5个字节
  private void writeVarint32(int n) throws TException {
    int idx = 0; //index flag
    while (true) {
      if ((n & ~0x7F) == 0) { // if (n <= 2^7) 1byte
        i32buf[idx++] = (byte)n;
        // writeByteDirect((byte)n);
        break;
        // return;
      } else {
        i32buf[idx++] = (byte)((n & 0x7F) | 0x80); 、//else if(n > 2^ 7) 按小端方式给byte第八位贴上1标签,存放在buf。
        // writeByteDirect((byte)((n & 0x7F) | 0x80));
        n >>>= 7; //逻辑右移7bit,再次判断,loop
      }
    }
    trans_.write(i32buf, 0, idx); //吧buf写入传输层
  }

  /**
   * Write an i64 as a varint. Results in 1-10 bytes on the wire.
   */
  byte[] varint64out = new byte[10];//最大需要10bytes
  private void writeVarint64(long n) throws TException {
    int idx = 0;
    while (true) {
      if ((n & ~0x7FL) == 0) { //注意这边的 ~0x7FL(不能写成0x7F)
        varint64out[idx++] = (byte)n;
        break;
      } else {
        varint64out[idx++] = ((byte)((n & 0x7F) | 0x80));
        n >>>= 7;
      }
    }
    trans_.write(varint64out, 0, idx);
  }

上面注解说明了varint的系统操作,预分配最大字节buffer,然后按照小端方式写入VLQ编码后实际内容。再来看看系统是怎么结合两者的:

 public void writeI32(int i32) throws TException {
    writeVarint32(intToZigZag(i32)); //先调intToZigZag转换,在write VLQ。
  }

  /**
   * Write an i64 as a zigzag varint.
   */
  public void writeI64(long i64) throws TException {
    writeVarint64(longToZigzag(i64));
  }

   public void writeI16(short i16) throws TException { //i16先按int32 zigzag编码转换 然后按VLQ转换
    writeVarint32(intToZigZag(i16));
  }

我们先系统的看一下TCompactProtocol按什么方法写入Thrift内部数据类型的,然后再看message的写法,一下是thrift内部数据类型,i16,i32,i64已经看完,在来看看别的:

  private static class Types {
    public static final byte BOOLEAN_TRUE   = 0x01;
    public static final byte BOOLEAN_FALSE  = 0x02;
    public static final byte BYTE           = 0x03;
    public static final byte I16            = 0x04;
    public static final byte I32            = 0x05;
    public static final byte I64            = 0x06;
    public static final byte DOUBLE         = 0x07;
    public static final byte BINARY         = 0x08;
    public static final byte LIST           = 0x09;
    public static final byte SET            = 0x0A;
    public static final byte MAP            = 0x0B;
    public static final byte STRUCT         = 0x0C;
  }

boolean:

  public void writeBool(boolean b) throws TException {
    if (booleanField_ != null) {
      // we haven‘t written the field header yet
      writeFieldBeginInternal(booleanField_, b ? Types.BOOLEAN_TRUE : Types.BOOLEAN_FALSE);
      booleanField_ = null;
    } else {
      // we‘re not part of a field, so just write the value.
      writeByteDirect(b ? Types.BOOLEAN_TRUE : Types.BOOLEAN_FALSE);//按照上面对应的boolean_yes,boolean_no字节值写入。
    }
  }

TCompactProtocol写入Boolean分两种情况,1:该boolean值为TStruct中的内部成员时TField时,得写入header数据(即内容和数据类型压缩在一起写);2 :如果不为TField内部类型的话,直接按byte写入。关于TStruct和TField的细节请参照上篇

具体tstruct写入,稍后分析。

byte:

public void writeByte(byte b) throws TException {
    writeByteDirect(b);//one byte 直接写入。
  }
 private byte[] byteDirectBuffer = new byte[1];
  private void writeByteDirect(byte b) throws TException {
    byteDirectBuffer[0] = b;
    trans_.write(byteDirectBuffer);
  }

double:

  public void writeDouble(double dub) throws TException {
    byte[] data = new byte[]{0, 0, 0, 0, 0, 0, 0, 0}; //8个字节
    fixedLongToBytes(Double.doubleToLongBits(dub), data, 0); //double 转long bit 分布,然后按照fix64编码传输。
    trans_.write(data);
  }
  private void fixedLongToBytes(long n, byte[] buf, int off) {
    buf[off+0] = (byte)( n        & 0xff);
    buf[off+1] = (byte)((n >> 8 ) & 0xff);
    buf[off+2] = (byte)((n >> 16) & 0xff);
    buf[off+3] = (byte)((n >> 24) & 0xff);
    buf[off+4] = (byte)((n >> 32) & 0xff);
    buf[off+5] = (byte)((n >> 40) & 0xff);
    buf[off+6] = (byte)((n >> 48) & 0xff);
    buf[off+7] = (byte)((n >> 56) & 0xff);
  }

可以看出double类型,先按Double.doubletoLongBits()转换后,按照fixed64编码写入(8字节小端写入),如上。

bytearray:

 public void writeBinary(ByteBuffer bin) throws TException {
    int length = bin.limit() - bin.position();//计算数据len
    writeBinary(bin.array(), bin.position() + bin.arrayOffset(), length);
  }
private void writeBinary(byte[] buf, int offset, int length) throws TException {
    writeVarint32(length); //按VLQ编码写入len值,这里没有使用zigzag编码(zigzag编码主要解决负数VLQ编码占用大空间的情况,这里len不为负,直接VLQ写入)
    trans_.write(buf, offset, length);//写入实际内buff中内容
  }

string:

 public void writeString(String str) throws TException {
    try {
      byte[] bytes = str.getBytes("UTF-8");//utf-8编码,得到字节数组
      writeBinary(bytes, 0, bytes.length);//抵用writeBinary,see 上面
    } catch (UnsupportedEncodingException e) {
      throw new TException("UTF-8 not supported!");
    }
  }

容器类型:

SetTag:

public void writeSetBegin(TSet set) throws TException {
    writeCollectionBegin(set.elemType, set.size);//set类型,长度值
  }

type byte:

public final class TType {
  public static final byte STOP   = 0;
  public static final byte VOID   = 1;//java中没有这种类型,这里存在只是为了别的语言,可能
  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;
  public static final byte SET    = 14;
  public static final byte LIST   = 15;
  public static final byte ENUM   = 16;//低下static {}中,该类型也没用到。 所以4bits 够用了
}
 protected void writeCollectionBegin(byte elemType, int size) throws TException {
    if (size <= 14) { // 1110
      writeByteDirect(size << 4 | getCompactType(elemType));//size <= 14时,size << 4 | 对应的TTyte,压缩从一个byte写入。
    } else {
      writeByteDirect(0xf0 | getCompactType(elemType));// 1111 0000| ttype ,按one byte写入
      writeVarint32(size);// VLQ编码写入len
    }
  }

getCompactType(xx):

 private byte getCompactType(byte ttype) {
    return ttypeToCompactType[ttype];
  }
static {
    ttypeToCompactType[TType.STOP] = TType.STOP;
    ttypeToCompactType[TType.BOOL] = Types.BOOLEAN_TRUE;
    ttypeToCompactType[TType.BYTE] = Types.BYTE;
    ttypeToCompactType[TType.I16] = Types.I16;
    ttypeToCompactType[TType.I32] = Types.I32;
    ttypeToCompactType[TType.I64] = Types.I64;
    ttypeToCompactType[TType.DOUBLE] = Types.DOUBLE;
    ttypeToCompactType[TType.STRING] = Types.BINARY;
    ttypeToCompactType[TType.LIST] = Types.LIST;
    ttypeToCompactType[TType.SET] = Types.SET;
    ttypeToCompactType[TType.MAP] = Types.MAP;
    ttypeToCompactType[TType.STRUCT] = Types.STRUCT;
  }
public void writeListEnd() throws TException {} //no-op 空操作,走个形式而已

list tag:

 public void writeListBegin(TList list) throws TException {
    writeCollectionBegin(list.elemType, list.size);
  }
public void writeListEnd() throws TException {}

同上,就不重复了。

map tag:

public void writeMapBegin(TMap map) throws TException {
    if (map.size == 0) {//size == 0
      writeByteDirect(0); //直接写入one byte 0完事。
    } else {
      writeVarint32(map.size); //VLQ写入长度
      writeByteDirect(getCompactType(map.keyType) << 4 | getCompactType(map.valueType)); //one byte 写入 keyType(TType),valueType(TType)  (keyType << 4 | valueType) 与avro的map不同,其key
    }                                                                                    //type只能为string类型。
  }

wirteMapEnd()也是no-op操作就不贴了。

介绍完内置类型的写入方式,可以介绍写message了。

public void writeMessageBegin(TMessage message) throws TException {
    writeByteDirect(PROTOCOL_ID); // 1000 0010 one byte protocol_id
    writeByteDirect((VERSION & VERSION_MASK) | ((message.type << TYPE_SHIFT_AMOUNT) & TYPE_MASK));// ((0000 0001 & 0001 1111) | (type << 5)) & 1110 0000); one byte高三位messageType |
    writeVarint32(message.seqid);                                                          //低五位version bits,   VLQ编码写入message 的sequence increment id.
    writeString(message.name);  //消息名,即方法名。
  }
 private static final byte PROTOCOL_ID = (byte)0x82;//1000 0010
  private static final byte VERSION = 1;
  private static final byte VERSION_MASK = 0x1f; // 0001 1111
  private static final byte TYPE_MASK = (byte)0xE0; // 1110 0000
  private static final byte TYPE_BITS = 0x07; // 0000 0111
  private static final int  TYPE_SHIFT_AMOUNT = 5;

这里的version应该为了以后的version更新。byte类型的messageType(call, execption, oneway,reply)具体请见上篇TBinaryProtocol分析。为了发消息的完整性,还是贴出TServiceClient的sendBase()步骤:

 protected void sendBase(String methodName, TBase args) throws TException {
    oprot_.writeMessageBegin(new TMessage(methodName, TMessageType.CALL, ++seqid_));
    args.write(oprot_);
    oprot_.writeMessageEnd();
    oprot_.getTransport().flush();
  }

现在该进行TBASE的write()了,即方法参数和返回值的封装类写,还是以hello.thrift为例:

hellostring_args的write():

public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException {
      schemes.get(oprot.getScheme()).getScheme().write(oprot, this);
    }

schema的write():

public void write(org.apache.thrift.protocol.TProtocol oprot, helloString_args struct) throws org.apache.thrift.TException {
        struct.validate();

        oprot.writeStructBegin(STRUCT_DESC);
        if (struct.para != null) {
          oprot.writeFieldBegin(PARA_FIELD_DESC);
          oprot.writeString(struct.para);
          oprot.writeFieldEnd();
        }
        oprot.writeFieldStop();
        oprot.writeStructEnd();
      }
 private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("helloString_args");// 方法参数封装类的TStruct表示。

 private static final org.apache.thrift.protocol.TField PARA_FIELD_DESC = new org.apache.thrift.protocol.TField("para", org.apache.thrift.protocol.TType.STRING, (short)1);

ok,此处的oprot为TCompactProtocol,看看他的writeStructBegin():

 public void writeStructBegin(TStruct struct) throws TException {
    lastField_.push(lastFieldId_);记住上次write struct 最后的field id.
    lastFieldId_ = 0; //从本次参数写开始。
  }
private ShortStack lastField_ = new ShortStack(15); //用于存放Tstructs中的field id(也就是thrift定义文件中service方法参数的标号 1:,2:);用于跟踪当前struct或者之前struct的field id

接下来,写writeFieldBegin()吧:

 public void writeFieldBegin(TField field) throws TException {
    if (field.type == TType.BOOL) { //如果该方法参数为boolean类型,
      // we want to possibly include the value, so we‘ll wait.
      booleanField_ = field; //这里先做下标记,等会和具体boolean值一块写,压缩嘛!一开始介绍些基本数据类型(上面)的boolean的两种情况,第一种指当boolean值为Tfield的话,压缩一下,跟这里相结合,
    } else {                 //这里先记录下header metadata,等写实际内容时,即writeBoolean在一块写。
      writeFieldBeginInternal(field, (byte)-1);
    }
  }
private void writeFieldBeginInternal(TField field, byte typeOverride) throws TException {
    // short lastField = lastField_.pop();

    // if there‘s a type override, use that. // -1获得其内置数据类型,如果非-1情况,(指的是boolean)直接写入其byte值 ,true 0x01,false 0x02
    byte typeToWrite = typeOverride == -1 ? getCompactType(field.type) : typeOverride; // typeOverride为写Boolean值,特设的,对其优化,one byte写入

    // check if we can use delta encoding for the field id 增量式编码前提,用one byte 4MSB来做增量式编码,所有field id之间的差不能大于15.每次写Tstruct(即一个方法参数的封装类,其中可能含有很多参数)
    if (field.id > lastFieldId_ && field.id - lastFieldId_ <= 15) { // 因为每次写struct时,都会设置last_fieldid_ = 0,所以都是一次方法RPC调用参数表示ID之间的比较。不会出现上次RPC方法调用的参数id和
      // write them together                      //本次RPC方法调用参数id的比较。 
      writeByteDirect((field.id - lastFieldId_) << 4 | typeToWrite);  //本次field id和上次field id做增量 << 4和复写标志做 |,用一个byte传输,压缩空间。
    } else {
      // write them separate
      writeByteDirect(typeToWrite); //分开写 one byte 复写标志。
      writeI16(field.id); //i16 (zigzag + vlq编码)写入,参数个数最大2^16个。
    }

    lastFieldId_ = field.id; //重新复制lastfield_id
    // lastField_.push(field.id);
  }

然后就是写具体的参数值内容了,写完后写上writeFieldEnd()操作;

structs所有的参数都写完后,调用writeFieldStop():

 public void writeFieldStop() throws TException {
    writeByteDirect(TType.STOP);// one byte value 0,占位符吧,标志读完了。
  }

writeStructEnd():

  public void writeStructEnd() throws TException {
    lastFieldId_ = lastField_.pop();//重新写structs时,会吧这值压入stack,并重新附上0.
  }
public void writeMessageEnd() throws TException {}

读操作就不分析了,朋友们可以参照了去看看。

时间: 2024-09-30 19:52:45

Thrift之TProtocol系列TCompactProtocol解析的相关文章

Thrift之TProtocol系列TBinaryProtocol解析

首先看一下Thrift的整体架构,如下图: 如图所示,黄色部分是用户实现的业务逻辑,褐色部分是根据thrift定义的服务接口描述文件生成的客户端和服务器端代码框架(前篇2中已分析了thrift service生成代码),红色部分是根据Thrift文件生成代码实现数据的读写操作.红色部分以下是Thrift的协议,传输体系以及底层的IO通信,使用thrift可以很方便的定义一个服务并且选择不同的传输协议和传输层而不用重新生成代码(Thrift提供的是一种大而全的服务,它认为没有统一的标准,用户根据自

Thrift之TProtocol系列TJSONProtocol解析

在了解JSON协议之前,朋友们可以先去了解一下JSON的基础知识,和ASCII基本分布,关于JSON一些常识请见这里; JSON (JavaScript Object Notation)是一种数据交换格式,是以JavaScript为基础的数据表示语言,是在以下两种数据结构的基础上来定义基本的数据描述格式的:1) 含有名称/值对的Object:2) 以”[“,",","]"组成的数组.对于 JSON,下例:形如{“name”:”tom”,”age”:23}就表示一个J

[转]Android自定义控件三部曲系列完全解析(动画, 绘图, 自定义View)

来源:http://blog.csdn.net/harvic880925/article/details/50995268 一.自定义控件三部曲之动画篇 1.<自定义控件三部曲之动画篇(一)——alpha.scale.translate.rotate.set的xml属性及用法>2.<自定义控件三部曲之动画篇(二)——Interpolator插值器>3.<自定义控件三部曲之动画篇(三)—— 代码生成alpha.scale.translate.rotate.set及插值器动画&g

ansible源码分析系列---inventory解析

host_list有以下几种类型:1.能够以","分割的字符串,2.为None,3.为python列表类型,4.目录,5.文件(yaml格式.ini格式).

pwn入门系列习题解析(二)

第一题--BITSCTF 2017-Command_Line 查看文件格式以及开启的保护措施,此处全保护均未开启(默认开启ASLR),且为64位ELF. 尝试运行,发现打印出一处地址(基本不用考虑ASLR了),猜测为栈某处地址 放入ida观察逻辑,发现的确打印了栈上的一个地址,可以直接用.此处可以顺便探测一下偏移,0x10+8=0x18,输入0x18个字符后即可覆盖ret.只要注意shellcode位于泄露的栈地址后的0x20处(0x18+8=0x20).至于shellcode直接从网上找就可以

Apache Thrift系列详解(一)- 概述与入门

前言Thrift是一个轻量级.跨语言的远程服务调用框架,最初由Facebook开发,后面进入Apache开源项目.它通过自身的IDL中间语言, 并借助代码生成引擎生成各种主流语言的RPC服务端/客户端模板代码. Thrift支持多种不同的编程语言,包括C++.Java.Python.PHP.Ruby等,本系列主要讲述基于Java语言的Thrift的配置方式和具体使用. 正文Thrift的技术栈Thrift对软件栈的定义非常的清晰, 使得各个组件能够松散的耦合, 针对不同的应用场景, 选择不同是方

Thrift compiler代码生成类解析

代码生成类解析: Thrift--facebook RPC框架,介绍就不说了,百度,google一大把,使用也不介绍,直接上结构和分析吧. Hello.thrift文件内容如下: namespace java com.tomsun.thrift.generated.demo service Hello { string helloString(1:string para) } 内容很简单,申明个RPC service(Hello),服务方法helloString,方法参数格式(seq: para

thrift系列 - 多语言实例 for java和python

1.简述 本文主要介绍thrift多语言.跨语言的代码实例.Thrift对多语言的支持非常不错,定义一个thrift接口文件,通过thrift IDL compiler(代码生成引擎)生成各个语言的代码,将各自语言的代码放入各自语言的工程中,写好服务端和客户端程序,通信的问题即刻解决. 2.简单架构图 示例的thrift接口文件,test8.thrift: service TestService { string test(1: i32 num,2: string name) } 代码生成方法,

Thrift源码解析--TBinaryProtocol

本文为原创,未经许可禁止转载. 关于Tprotocol层都是一些通信协议,个人感觉内容较大,很难分类描述清楚.故打算以TBinaryProtocol为例,分析客户端发请求以及接收服务端返回数据的整个过程. 先将客户端的测试用例贴上. 1 public class DemoClient { 2 public static void main(String[] args) throws Exception{ 3 String param1 = "haha"; 4 Map<String