您的位置:首页 > Web前端 > JavaScript

Thrift之TProtocol系列TJSONProtocol解析

2015-06-30 21:03 816 查看
在了解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字符表:

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


前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},
....................................................................
}
]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: