Thrift之TProtocol系列TJSONProtocol解析

      在了解JSON协议之前,朋友们可以先去了解一下JSON的基础知识,和ASCII基本分布,关于JSON一些常识请见这里;

      JSON (JavaScript Object Notation)是一种数据交换格式,是以JavaScript为基础的数据表示语言,是在以下两种数据结构的基础上来定义基本的数据描述格式的:1) 含有名称/值对的Object;2) 以”[“,",","]"组成的数组。对于 JSON,下例:形如{“name”:”tom”,”age”:23}就表示一个JSON 对象,其有两个属性,值分别为tom和23。key必须为string。JSON支持的基本数据类型,包括Number,Boolean,  null, String;  boolean, null类型值不能加” “,不然会当做string来出来,再通过object, array来组合成丰富的类型结构。

    来看看Thrift中JSON常用标签的定义:

  private static final byte[] COMMA = new byte[] {','};      //json object中键值对之间, json array中元素之间的
  private static final byte[] COLON = new byte[] {':'};       //json object中key : value
  private static final byte[] LBRACE = new byte[] {'{'};      //json object开始标签
  private static final byte[] RBRACE = new byte[] {'}'};      //json object结束标签
  private static final byte[] LBRACKET = new byte[] {'['};    //json array开始标签
  private static final byte[] RBRACKET = new byte[] {']'};    //json array结束标签
  private static final byte[] QUOTE = new byte[] {'"'};       //json 字符串标签
  private static final byte[] BACKSLASH = new byte[] {'\\'};   //json中转义
  private static final byte[] ZERO = new byte[] {'0'};        // 0字符

      Unicode编码形式:

private static final byte[] ESCSEQ = new byte[] {'\\','u','0','0'};

      JSON中字符串是由双引号包围的任意数量的unicode字符结合,当然还要包含通过'\'转义字符(不多),如下;

  private static final String ESCAPE_CHARS = "\"\\/bfnrt";    //需要转义的字符 '"' , '\' , ' /' ,'b' , 'f' , 'n' ,'r' ,'t'
  

  private static final byte[] ESCAPE_CHAR_VALS = {
    '"', '\\', '/', '\b', '\f', '\n', '\r', '\t', // 转义字符对应的字节数组
  };

    ASCII表前48字符表:

1   private static final byte[] JSON_CHAR_TABLE = {
2     /*  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F */
3         0,  0,  0,  0,  0,  0,  0,  0,'b','t','n',  0,'f','r',  0,  0, // 0
4         0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 1
5         1,  1,'"',  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, // 2
6   };

     前48个字符中,值为0的是一些不常用的控制字符,所以这里标为0,unicode输出;值为1的为那些可显示非转义字符,输出时直接按其ASCII编码值输出即可;其他的是需要转义的字符,按照上面的转义字符的字节数组输出,

下面是ASCII编码图,朋友们可以参照着看一下:

 

     下面是Thrift内部数据类型对应的JSON输出字节数组标志,在write和read时,会按照其进行相应的转换,源码时,具体分析:

  private static final byte[] NAME_BOOL = new byte[] {'t', 'f'};
  private static final byte[] NAME_BYTE = new byte[] {'i','8'};
  private static final byte[] NAME_I16 = new byte[] {'i','1','6'};
  private static final byte[] NAME_I32 = new byte[] {'i','3','2'};
  private static final byte[] NAME_I64 = new byte[] {'i','6','4'};
  private static final byte[] NAME_DOUBLE = new byte[] {'d','b','l'};
  private static final byte[] NAME_STRUCT = new byte[] {'r','e','c'};
  private static final byte[] NAME_STRING = new byte[] {'s','t','r'};
  private static final byte[] NAME_MAP = new byte[] {'m','a','p'};
  private static final byte[] NAME_LIST = new byte[] {'l','s','t'};
  private static final byte[] NAME_SET = new byte[] {'s','e','t'};

      基本重要数据成员介绍完毕,在具体分析读写前,再来介绍下写辅助方法和类,有助于后面读写的具体理解:

// 把char('0 - 9' & ' a - f')字符转变为相应的hex十六进制值。
  private static final byte hexVal(byte ch) throws TException {
    if ((ch >= '0') && (ch <= '9')) {
      return (byte)((char)ch - '0');
    }
    else if ((ch >= 'a') && (ch <= 'f')) {
      return (byte)((char)ch - 'a' + 10);
    }
    else {
      throw new TProtocolException(TProtocolException.INVALID_DATA,
                                   "Expected hex character");
    }
  }

  // 上面的反过程,把hex十六进制值转变为相应的字符。
  private static final byte hexChar(byte val) {
    val &= 0x0F;
    if (val < 10) {
      return (byte)((char)val + '0');
    }
    else {
      return (byte)((char)(val - 10) + 'a');
    }
  }

     辅助类,用于具体读写过程中写入,或读取JSON语法字符,该类为基类,no-op:

 protected class JSONBaseContext {
    protected void write() throws TException {}

    protected void read() throws TException {}

    protected boolean escapeNum() { return false; }
  }

     用于读取JSON数组的JSONBASEContext子类, 用于写入,读取元素item之间的JSON语法字符 ' ,':

 protected class JSONListContext extends JSONBaseContext {
    private boolean first_ = true;  //第一个元素之前不要添加,读取 ',';

    @Override
    protected void write() throws TException {
      if (first_) {
        first_ = false;
      } else {
        trans_.write(COMMA);
      }
    }

    @Override
    protected void read() throws TException {
      if (first_) {
        first_ = false;
      } else {
        readJSONSyntaxChar(COMMA);
      }
    }
  }

     用于读取JSON 对象(Object)的JSONBASEContext子类,  用于写入,读取元素item之间的 ' ,' , 和key, value对之间的 ':' JSON语法字符:

  protected class JSONPairContext extends JSONBaseContext {
    private boolean first_ = true;   //添加,读取 ','字符的标志,第一个不需要加入和读取;
    private boolean colon_ = true;   //添加,读取 ':'字符的标志,

    @Override
    protected void write() throws TException {
      if (first_) {
        first_ = false;
        colon_ = true;
      } else {
        trans_.write(colon_ ? COLON : COMMA);
        colon_ = !colon_;
      }
    }

    @Override
    protected void read() throws TException {
      if (first_) {
        first_ = false;
        colon_ = true;
      } else {
        readJSONSyntaxChar(colon_ ? COLON : COMMA);
        colon_ = !colon_;
      }
    }

    @Override
    protected boolean escapeNum() { //由于JSON Object的key必须为string类型,所以这里用colon标志,同时标注是否需要写入,读取'"'字符;
      return colon_;
    }
  }

   

private Stack<JSONBaseContext> contextStack_ = new Stack<JSONBaseContext>(); //用于读取不同类型时,上下文上下切换,上面提及的两种上下文,读取和写入JSON Object,和JSON array,当递归解析和写入时,得随时切换
private JSONBaseContext context_ = new JSONBaseContext(); //当前读取,写入上下文。
// private void pushContext(JSONBaseContext c) { //吧方法参数的上下文设为当前上下文(比如当前真在解析JSON array,而array中出现一个JOSN object(map) item时,得切换到JSONPairContext来解析) contextStack_.push(context_); //当前上下文压入栈, context_ = c; } private void popContext() { context_ = contextStack_.pop(); //吧之前压入栈的上下文设为当前上下文(还是上面的例子,当JSON object解析完后,接着解析array别的item,所以还得之前压入栈的上下文) }

 

       上面两个类都用到readJSONSyntaxChar(xxx)方法,我们再来看看:

 protected void readJSONSyntaxChar(byte[] b) throws TException {
    byte ch = reader_.read();     //方法参数设定一个字节数组,当读取的字节不等于b[0]抛异常,要来验证读取的字节是否为指定的JSON语句字符的。
    if (ch != b[0]) {
      throw new TProtocolException(TProtocolException.INVALID_DATA,
                                   "Unexpected character:" + (char)ch);
    }
  }

       JOSN协议从传输层读取字节时都是一字节一字节的读取:

 protected class LookaheadReader {

    private boolean hasData_;
    private byte[] data_ = new byte[1];

   //检测当前容量为1的字节数组中是否有数据,没有从底层传输层读取一字节,该方法是消耗型的
    protected byte read() throws TException {
      if (hasData_) {
        hasData_ = false;
      }
      else {
        trans_.readAll(data_, 0, 1);
      }
      return data_[0];
    }

    // 同上,不同的是该方法不发生消耗
    protected byte peek() throws TException {
      if (!hasData_) {
        trans_.readAll(data_, 0, 1);
      }
      hasData_ = true;
      return data_[0];
    }
  }

     别的参数成员:

 // 上面已经介绍,当前解析的底层读取器,一个字节一个字节的读取
  private LookaheadReader reader_ = new LookaheadReader();

  // 是否写入TField的(即方法参数的参数名)的标志,不写入参数名,就要参数标号来代替
  private boolean fieldNamesAsString_ = false;

    OK!进入正文,首先看看JSON协议怎么写数据的,从string开始,JSON String分Unicode和转义字符两种:

 private void writeJSONString(byte[] b) throws TException {
    context_.write(); // JSONBaseContext 空操作
    trans_.write(QUOTE); //写入'"'字符
    int len = b.length;  
    for (int i = 0; i < len; i++) {
      if ((b[i] & 0x00FF) >= 0x30) { //对照上面的ASCII表, >= 0x30即 >= 48(上面JSON_CHAR_TABLE表述的为0 - 47,48以上的用的着的转义字符就'\')
        if (b[i] == BACKSLASH[0]) { //如果写入的字符为 '\',需要转义。
          trans_.write(BACKSLASH);//
          trans_.write(BACKSLASH); // 写两次'\'
        }
        else {
          trans_.write(b, i, 1);// 0x30上除'\',之外的字符直接写入
        }
      }
      else { // b[i] < 48的情况,即上面设计的那张JSON_CHAR_TABLE倍
        tmpbuf_[0] = JSON_CHAR_TABLE[b[i]];  
        if (tmpbuf_[0] == 1) {       //查表后,表对应的值为1的情况:非转义,即可直接写入。
          trans_.write(b, i, 1);
        }
        else if (tmpbuf_[0] > 1) {     //即为那些转义字符 '"' , '/' , 'b' , 't' , 'r' , 'n';
          trans_.write(BACKSLASH); // 写之前先追加'\';
          trans_.write(tmpbuf_, 0, 1);
        }
        else { // 0
          trans_.write(ESCSEQ);//unicode   \u00xx形式写入。
          tmpbuf_[0] = hexChar((byte)(b[i] >> 4)); 
          tmpbuf_[1] = hexChar(b[i]);
          trans_.write(tmpbuf_, 0, 2);
        }
      }
    }
    trans_.write(QUOTE);  // 最后再加上'"';
  }

      JSON 整形输出:

private void writeJSONInteger(long num) throws TException { //虽然这里取long的字符串表现形式,但是输出是按JSON 整数表示输出(没加 '"');
    context_.write();  //no -op
    String str = Long.toString(num); //string表示形式
    boolean escapeNum = context_.escapeNum();  //false
    if (escapeNum) {
      trans_.write(QUOTE);  
    }
    try {  
      byte[] buf = str.getBytes("UTF-8");  //UTF-8字符编码得到字节数组
      trans_.write(buf);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    if (escapeNum) {
      trans_.write(QUOTE);
    }
  }

     JSON 浮点数写入,JSON浮点数有几点特殊,JSON几种特殊的浮点数用字符串表示和传输:

     a. NaN表示不是数字值;

     b. Infinity表示正无穷大;

     c. –Infinity表示负无穷大。

private void writeJSONDouble(double num) throws TException {
    context_.write();
    String str = Double.toString(num);
    boolean special = false;
    switch (str.charAt(0)) {
    case 'N': // NaN
    case 'I': // Infinity
      special = true;
      break;
    case '-':
      if (str.charAt(1) == 'I') { // -Infinity
        special = true;
      }
      break;
    default:
      break;
  }

    boolean escapeNum = special || context_.escapeNum();  //三种特殊情况,special标志位true,此时escapeNum为true, 即用JSON字符串形式表示和输出,否则还是以JSON Number类型输出
    if (escapeNum) {
      trans_.write(QUOTE); // '"'
    }
    try {
      byte[] b = str.getBytes("UTF-8");
      trans_.write(b, 0, b.length);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    if (escapeNum) {
      trans_.write(QUOTE); // '"'
    }
  }

       Thrift的二进制值并编码为base64编码然后作为JSON的字符串:

   public void writeBinary(ByteBuffer bin) throws TException {
      writeJSONBase64(bin.array(), bin.position() + bin.arrayOffset(), bin.limit() - bin.position() - bin.arrayOffset());
   }


private void writeJSONBase64(byte[] b, int offset, int length) throws TException { context_.write(); trans_.write(QUOTE); int len = length; int off = offset; while (len >= 3) { // Encode 3 bytes at a time TBase64Utils.encode(b, off, 3, tmpbuf_, 0); trans_.write(tmpbuf_, 0, 4); off += 3; len -= 3; } if (len > 0) { // Encode remainder TBase64Utils.encode(b, off, len, tmpbuf_, 0); trans_.write(tmpbuf_, 0, len + 1); } trans_.write(QUOTE); }

      Thrift的bool类型写入方式是按JSON 整形写入,false = 0 , ture = 1:

public void writeBool(boolean b) throws TException {
    writeJSONInteger(b ? (long)1 : (long)0);
  }

    Thrift其他整型和字节写入方式一并贴出,转换为long类型,都是按JSON整形写入:

 @Override
  public void writeByte(byte b) throws TException {
    writeJSONInteger((long)b);
  }

  @Override
  public void writeI16(short i16) throws TException {
    writeJSONInteger((long)i16);
  }

  @Override
  public void writeI32(int i32) throws TException {
    writeJSONInteger((long)i32);
  }

  @Override
  public void writeI64(long i64) throws TException {
    writeJSONInteger(i64);
  }

   Thrift的double类型,按照JSON浮点数写入:

 public void writeDouble(double dub) throws TException {
    writeJSONDouble(dub);
  }

   Thrift的string类型按照JSON字符串写入,JSON在处理时,会对一些字符进行转义:

 public void writeString(String str) throws TException {
    try {
      byte[] b = str.getBytes("UTF-8"); //string utf-8编码之后得到字节数组
      writeJSONString(b);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
  }

    再来看看Thrift 的set , list写入方法:

  @Override
  public void writeListBegin(TList list) throws TException {
    writeJSONArrayStart();
    writeJSONString(getTypeNameForTypeID(list.elemType));
    writeJSONInteger(list.size);
  }

  @Override
  public void writeListEnd() throws TException {
    writeJSONArrayEnd();
  }

  @Override
  public void writeSetBegin(TSet set) throws TException {
    writeJSONArrayStart();
    writeJSONString(getTypeNameForTypeID(set.elemType));
    writeJSONInteger(set.size);
  }

  @Override
  public void writeSetEnd() throws TException {
    writeJSONArrayEnd();
  }

     Thrift的lists和sets被表示为JSON的array(数组),其中数组的第一个元素表示Thrift元素的数据类型,数组第二值表示后面Thrift元素的个数。接着后面就是所有的元素:

  private void writeJSONArrayStart() throws TException {
    context_.write();
    trans_.write(LBRACKET);  //写入JSON array语法字符 ‘[’
    pushContext(new JSONListContext()); //设置当前处理解析上下文,
  }

  private void writeJSONArrayEnd() throws TException {
    popContext();   //回复之前的处理解析上下文
    trans_.write(RBRACKET);  //补上JSON array语句结束字符']'
  }

      数组的第一个元素为数据类型,按JSON字符串写入,即本篇开头贴出的代码,getTypeNameForTypeID(set.elemType) 用于Thrift数据类型和JSON为该类型表示的字符串的字节数组之间的转换:

 private static final byte[] getTypeNameForTypeID(byte typeID)
    throws TException {
    switch (typeID) {
    case TType.BOOL:
      return NAME_BOOL;
    case TType.BYTE:
      return NAME_BYTE;
    case TType.I16:
      return NAME_I16;
    case TType.I32:
      return NAME_I32;
    case TType.I64:
      return NAME_I64;
    case TType.DOUBLE:
      return NAME_DOUBLE;
    case TType.STRING:
      return NAME_STRING;
    case TType.STRUCT:
      return NAME_STRUCT;
    case TType.MAP:
      return NAME_MAP;
    case TType.SET:
      return NAME_SET;
    case TType.LIST:
      return NAME_LIST;
    default:
      throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED,
                                   "Unrecognized type");
    }
  }

     数组的第二个元素为整个list , set的长度,按JSON整形写入,然后写入各个容器成员。

     Thrift map数据类型写入方式:

  public void writeMapBegin(TMap map) throws TException {
    writeJSONArrayStart(); // '[' ,设置当前处理解析上下文
    writeJSONString(getTypeNameForTypeID(map.keyType)); //数组的第一个元素为key类型 =>JSON的类型字符串表示字节数组
    writeJSONString(getTypeNameForTypeID(map.valueType)); // 数组的第二个元素  为 value类型
    writeJSONInteger(map.size); //数组的第三个元素,map的长度
    writeJSONObjectStart(); //map的key,value对按 Json object输出。
  }

  @Override
  public void writeMapEnd() throws TException {
    writeJSONObjectEnd(); //回复当前上下文 ‘}’
    writeJSONArrayEnd(); //回复当前上下文 ']'
  }
  private void writeJSONObjectStart() throws TException {
    context_.write();
    trans_.write(LBRACE);
    pushContext(new JSONPairContext());
  }

  private void writeJSONObjectEnd() throws TException {
    popContext();
    trans_.write(RBRACE);
  }

       Thrift的maps被表示为JSON的array,其中前两个值分别表示键和值的类型,跟着就是键值对的个数,接着就是包含具体键值对的JSON对象了。注意了JSON的键只能是字符串,这就是要求Thrift的maps类型的键必须是数字和字符串的,并且数字和字符串做转换,就是下面提及的字符串。

       有效的类型标识是:"tf"代表 bool,"i8" 表示 byte,"i16"表示16位整数,"i32"表示32位整数,"i64"表示64位整数,"dbl"表示双精度浮点型,"str" 表示字符串(包括二进制),"rec"表示结构体 ("records"),"map"表示 map,"lst" 表示 list, "set" 表示set。

 

     Thrift的messages(消息)被表示为JSON的array,前四个元素分别代表协议版本、消息名称、消息类型和序列ID:

public void writeMessageBegin(TMessage message) throws TException {
    writeJSONArrayStart();
    writeJSONInteger(VERSION);
    try {
      byte[] b = message.name.getBytes("UTF-8");
      writeJSONString(b);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    writeJSONInteger(message.type);
    writeJSONInteger(message.seqid);
  }

    然后呢?该写Thrift的tstructs了(具体的RPC方法参数封装),struct是按照JSON object写入:

public void writeStructBegin(TStruct struct) throws TException {
    writeJSONObjectStart();
  }

   Thrift的TField(具体的每个参数)的写入:

 public void writeFieldBegin(TField field) throws TException {
    if (fieldNamesAsString_) {
      writeString(field.name); //参数名
    } else {
      writeJSONInteger(field.id); //或者参数id
    }
    writeJSONObjectStart();  // '{'
    writeJSONString(getTypeNameForTypeID(field.type)); Thrift数据类型的字符串表示
  }

   即Thrift的消息类型形如:

[ 1 (version) ,   "messageName" ,  type (call, oneway, reply, exception (byte)) ,  seq id , 
   {  "argument name" : {  " argument  type"  :  value  },
       "argument name" : {  " argument  type" :   value},
     ....................................................................
   }
]

 

posted @ 2015-06-30 21:03  TomSun*star  阅读(4333)  评论(0编辑  收藏  举报