Thrift的紧凑型传输协议分析:
用一张图说明一下Thrift的TCompactProtocol中各个数据类型是怎么表示的。
报文格式编码:
bool类型:
一个字节。
如果bool型的字段是结构体或消息的成员字段并且有编号,一个字节的高4位表示字段编号,低4位表示bool的值(0001:true, 0010:false),即:一个字节的低4位的值(true:1,false:2).
如果bool型的字段单独存在,一个字节表示值,即:一个字节的值(true:1,false:2).
Byte类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一个字节的值.
I16类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一至三个字节的值.
I32类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一至五个字节的值.
I64类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一至十个字节的值.
double类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),八个字节的值.
注:把double类型的数据转成八字节保存,并用小端方式发送。
String类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一至五个字节的负载数据的长度,负载数据.
Struct类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),结构体负载数据,一个字节的结束标记.
MAP类型:
一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一至五个字节的map元素的个数,一个字节的键值类型组合(高4位键类型,低4位值类型),Map负载数据.
Set类型:
表示方式一:一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一个字节的元素个数和值类型组合(高4位键元素个数,低4位值类型),Set负载数据.
适用于Set中元素个数小于等于14个的情况。
表示方式二:一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一个字节的键值类型(高4位全为1,低4位值类型),一至五个字节的map元素的个数,Set负载数据.
适用于Set中元素个数大于14个的情况。
List类型:
表示方式一:一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一个字节的元素个数和值类型组合(高4位键元素个数,低4位值类型),List负载数据.
适用于Set中元素个数小于等于14个的情况。
表示方式二:一个字节的编号与类型组合(高4位编号偏移1,低4位类型),一个字节的键值类型(高4位全为1,低4位值类型),一至五个字节的map元素的个数,List负载数据.
适用于Set中元素个数大于14个的情况。
消息(函数)类型:
一个字节的版本,一个字节的消息调用(请求:0x21,响应:0x41,异常:0x61,oneway:0x81),一至五个字节的消息名称长度,消息名称,消息参数负载数据,一个字节的结束标记。
以上说明是基于相邻字段的编号小于等于15的情况。
如果字段相邻编号大于15,需要把类型和编号分开表示:用一个字节表示类型,一至五个字节表示编号偏移值。
阅读到这里,或许会疑问,为什么数值型的值用 “一至五个字节”表示?
原因:对数值进行压缩,压缩算法就是Varint,如下简单的说明一下什么是Varint数值压缩。
Varint数值压缩
一个整数一般是以32位来表示的,存储需要4个字节。
当如果整数大小在256以内,那么只需要用一个字节就可以存储这个整数,这样剩下的3个字节的存储空间空闲。
当如果整数大小在256到65536之间,那么只需要用两个字节就可以存储这个整数,这样剩下的2个字节的存储空间空闲。
当如果整数大小在65536到16777216之间,那么只需要用三个字节就可以存储这个整数,这样剩下的1个字节的存储空间空闲。
当如果整数大小在16777216到4294967296之间,那么需要用四个字节存储这个整数。
这时,Google引入了varint,把表示整数的空闲空间压缩,用这种思想来序列化整数。
这种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。
Varint将数按照7位分段,把一个整数压缩后存储。
Varint 中的每个字节的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。
其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,会用两个字节。
这样就可以实现数值压缩。
采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。
从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。
实现Varint32代码:
uint32_t TCompactProtocolT<Transport_>::writeVarint32(uint32_t n) {
uint8_t buf[5];
uint32_t wsize = 0; while (true) {
if ((n & ~0x7F) == 0) {
buf[wsize++] = (int8_t)n;
break;
} else {
buf[wsize++] = (int8_t)((n & 0x7F) | 0x80);
n >>= 7;
}
}
trans_->write(buf, wsize);
return wsize;
}
同样的方式实现Varint64代码:
uint32_t TCompactProtocolT<Transport_>::writeVarint64(uint64_t n) {
uint8_t buf[10];
uint32_t wsize = 0; while (true) {
if ((n & ~0x7FL) == 0) {
buf[wsize++] = (int8_t)n;
break;
} else {
buf[wsize++] = (int8_t)((n & 0x7F) | 0x80);
n >>= 7;
}
}
trans_->write(buf, wsize);
return wsize;
}
或许你会疑问,如果一个整数最高位和比较低位为1,也就是说负数用varint怎么压缩?
既然正数可以用varint很好的压缩,能不能把负数转变成正数后再用varint做数值压缩呢?
答案是:Yes.
怎么把负数转成正数:
引入一个叫Zigzag的算法,那Zigzag到底是什么呢?
Zigzag算法
正数:当前的数乘以2, zigzagY = x * 2
负数:当前的数乘以-2后减1, zigzagY = x * -2 - 1
用程序的移位表示就是:
(n << 1) ^ (n >> 31) //int32
(n << 1> ^ (n >> 63) //int64
代码表示:
/**
* Convert l into a zigzag long. This allows negative numbers to be
* represented compactly as a varint.
*/
template <class Transport_>
uint64_t TCompactProtocolT<Transport_>::i64ToZigzag(const int64_t l) {
return (l << 1) ^ (l >> 63);
} /**
* Convert n into a zigzag int. This allows negative numbers to be
* represented compactly as a varint.
*/
template <class Transport_>
uint32_t TCompactProtocolT<Transport_>::i32ToZigzag(const int32_t n) {
return (n << 1) ^ (n >> 31);
}
Thrift中对数值的发送做法是:先做zigzag得到一个数,再做varint数值压缩。
下面用一个例子说明一下Thrift的TCompactProtocol协议。
建一个rpc.thrift的IDL文件。
namespace go demo.rpc
namespace cpp demo.rpc
struct ArgStruct {
1:byte argByte,
2:string argString
3:i16 argI16,
4:i32 argI32,
5:i64 argI64,
6:double argDouble, } service RpcService {
list<string> funCall(
1:ArgStruct argStruct,
2:byte argByte,
3:i16 argI16,
4:i32 argI32,
5:i64 argI64,
6:double argDouble,
7:string argString,
8:map<string, string> paramMapStrStr,
9:map<i32, string> paramMapI32Str,
10:set<string> paramSetStr,
11:set<i64> paramSetI64,
12:list<string> paramListStr,
),
}
使用命令生成go代码
thrift --gen go -o src rpc.thrift
编写一个go的thrift客户端:
package main import (
"demo/rpc"
"fmt"
"git.apache.org/thrift.git/lib/go/thrift"
"net"
"os"
"time"
) func main() {
startTime := currentTimeMillis()
//transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
transportFactory := thrift.NewTTransportFactory()
//protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
//protocolFactory := thrift.NewTJSONProtocolFactory()
//protocolFactory := thrift.NewTSimpleJSONProtocolFactory()
protocolFactory := thrift.NewTCompactProtocolFactory() transport, err := thrift.NewTSocket(net.JoinHostPort("127.0.0.1", "8090"))
if err != nil {
fmt.Fprintln(os.Stderr, "error resolving address:", err)
os.Exit(1)
} useTransport := transportFactory.GetTransport(transport)
client := rpc.NewRpcServiceClientFactory(useTransport, protocolFactory)
if err := transport.Open(); err != nil {
fmt.Fprintln(os.Stderr, "Error opening socket to 127.0.0.1:8090", " ", err)
os.Exit(1)
}
defer transport.Close() argStruct := &rpc.ArgStruct{}
argStruct.ArgByte = 53
argStruct.ArgString = "str value"
argStruct.ArgI16 = 54
argStruct.ArgI32 = 12
argStruct.ArgI64 = 43
argStruct.ArgDouble = 11.22
paramMap := make(map[string]string)
paramMap["name"] = "namess"
paramMap["pass"] = "vpass"
paramMapI32Str := make(map[int32]string)
paramMapI32Str[10] = "val10"
paramMapI32Str[20] = "val20"
paramSetStr := make(map[string]bool)
paramSetStr["ele1"] = true
paramSetStr["ele2"] = true
paramSetStr["ele3"] = true
paramSetI64 := make(map[int64]bool)
paramSetI64[11] = true
paramSetI64[22] = true
paramSetI64[33] = true
paramListStr := []string{"l1.","l2."}
r1, e1 := client.FunCall(argStruct,
53, 54, 12, 34, 11.22, "login",
paramMap,paramMapI32Str,
paramSetStr, paramSetI64, paramListStr)
fmt.Println("Call->", r1, e1) endTime := currentTimeMillis()
fmt.Println("Program exit. time->", endTime, startTime, (endTime - startTime))
} func currentTimeMillis() int64 {
return time.Now().UnixNano() / 1000000
}
编写简单测试的go服务端:
package main import (
"demo/rpc"
"fmt"
"git.apache.org/thrift.git/lib/go/thrift"
"os"
) const (
NetworkAddr = ":8090"
) type RpcServiceImpl struct {
} func (this *RpcServiceImpl) FunCall(argStruct *rpc.ArgStruct,
argByte int8, argI16 int16, argI32 int32,
argI64 int64, argDouble float64, argString string,
paramMapStrStr map[string]string, paramMapI32Str map[int32]string,
paramSetStr map[string]bool, paramSetI64 map[int64]bool,
paramListStr []string) (r []string, err error) {
fmt.Println("-->FunCall:", argStruct)
r = append(r, "return 1 by FunCall.")
r = append(r, "return 2 by FunCall.")
return
} func main() {
//transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
transportFactory := thrift.NewTTransportFactory()
//protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
protocolFactory := thrift.NewTCompactProtocolFactory()
//protocolFactory := thrift.NewTJSONProtocolFactory()
//protocolFactory := thrift.NewTSimpleJSONProtocolFactory() serverTransport, err := thrift.NewTServerSocket(NetworkAddr)
if err != nil {
fmt.Println("Error!", err)
os.Exit(1)
} handler := &RpcServiceImpl{}
processor := rpc.NewRpcServiceProcessor(handler) server := thrift.NewTSimpleServer4(processor, serverTransport,transportFactory, protocolFactory)
fmt.Println("thrift server in", NetworkAddr)
server.Serve()
}
go build rpcclient.go生成可执行文件rpcclient后执行。
执行前抓包进行分析。
请求:
0000 82 21 01 07 66 75 6e 43 61 6c 6c 1c 13 35 18 09
0010 73 74 72 20 76 61 6c 75 65 14 6c 15 18 16 56 17
0020 71 3d 0a d7 a3 70 26 40 00 13 35 14 6c 15 18 16
0030 44 17 71 3d 0a d7 a3 70 26 40 18 05 6c 6f 67 69
0040 6e 1b 02 88 04 6e 61 6d 65 06 6e 61 6d 65 73 73
0050 04 70 61 73 73 05 76 70 61 73 73 1b 02 58 14 05
0060 76 61 6c 31 30 28 05 76 61 6c 32 30 1a 38 04 65
0070 6c 65 31 04 65 6c 65 32 04 65 6c 65 33 1a 36 16
0080 2c 42 19 28 03 6c 31 2e 03 6c 32 2e 00 响应:
0000 82 41 01 07 66 75 6e 43 61 6c 6c 09 00 28 14 72
0010 65 74 75 72 6e 20 31 20 62 79 20 46 75 6e 43 61
0020 6c 6c 2e 14 72 65 74 75 72 6e 20 32 20 62 79 20
0030 46 75 6e 43 61 6c 6c 2e 00
开始分析抓包的请求数据。
消息头分析:
第一个字节 82 表示:COMPACT协议版本。
COMPACT_PROTOCOL_ID = 0x082
第二个字节21表示:消息请求,如何计算得到21呢?
COMPACT_VERSION = 1
COMPACT_VERSION_MASK = 0x1f
COMPACT_TYPE_MASK = 0x0E0
COMPACT_TYPE_BITS = 0x07
COMPACT_TYPE_SHIFT_AMOUNT = 5
(COMPACT_VERSION & COMPACT_VERSION_MASK) | ((byte(typeId) << COMPACT_TYPE_SHIFT_AMOUNT) & COMPACT_TYPE_MASK)
消息请求的message TypeId为1,带入计算
(0x01 & 0x1f) | ((0x01 << 5) & 0xe0 = 0x01 | 0x20 & 0xe0 = 0x01 | 0x20 = 0x21
第三个字节 01 为varint后的流水号 01.
第四个字节 07 为varint后消息的长度 07.
字节 66 75 6e 43 61 6c 6c 为消息名称字符串 funCall
开始解析参数:
函数funCall的第一个参数:
1:ArgStruct argStruct,
字节1c 表示结构体,高4为1表示编号偏移1,低4为c表示类型 0x0c为结构体。
偏移自加1保存,用于下一个字段编号偏移计算。
argStruct.ArgByte = 53
argStruct.ArgString = "str value"
argStruct.ArgI16 = 54
argStruct.ArgI32 = 12
argStruct.ArgI64 = 43
argStruct.ArgDouble = 11.22
结构体的第一个成员;
字节 13 35 表示结构体第一个成员ArgByte,
高4为1表示编号偏移1,低4为3表示类型 0x03为字节类型,值35就是十进制赋值的53.
结构体的第二个成员;
字节 18 09 73 74 72 20 76 61 6c 75 65表示结构体第二个成员ArgString,
高4为1表示编号偏移1,低4位8表示类型 0x08为二进制字符串类型,
09 表示varint后字符串的长度 9,值73 74 72 20 76 61 6c 75 65为字符串"str value"
结构体的第三个成员;
字节 14 6c 表示结构体第一个成员ArgI16,
高4为1表示编号偏移1,低4为4表示类型 0x04为16位数值类型,值6c,二进制 110 1100,右移动一位,做zigzag解压后,得到 11 0110, 就是十进制赋值的54.
结构体的第四个成员;
字节 15 18 表示结构体第一个成员ArgI32,
高4为1表示编号偏移1,低4为5表示类型 0x05为32位数值类型,值18,二进制 1 1000,右移动一位,做zigzag解压后,得到 1100, 就是十进制赋值的12.
结构体的第五个成员;
字节 16 56 表示结构体第一个成员ArgI64,
高4为1表示编号偏移1,低4为6表示类型 0x06为64位数值类型,值56,二进制 101 0110,右移动一位,做zigzag解压后,得到 10 1011, 就是十进制赋值的43.
结构体的第六个成员;
字节 17 71 3d 0a d7 a3 70 26 40 表示结构体第一个成员ArgDouble,
高4为1表示编号偏移1,低4为7表示类型 0x07为double数值类型,值71 3d 0a d7 a3 70 26 40,为11.22.
结构体的结束标记
字节 00 表示结构体结束。
函数funCall的第二个参数:
2:byte argByte,
字节 13 35 表示ArgByte,
高4为1表示编号偏移1,低4为3表示类型 0x03为字节类型,值35就是十进制赋值的53.
函数funCall的第三个参数:
3:i16 argI16,
字节 14 6c 表示ArgI16,
高4为1表示编号偏移1,低4为4表示类型 0x04为16位数值类型,值6c,二进制 110 1100,右移动一位,做zigzag解压后,得到 11 0110, 就是十进制赋值的54.
函数funCall的第四个参数:
4:i32 argI32,
字节 15 18 表示ArgI32,
高4为1表示编号偏移1,低4为5表示类型 0x05为32位数值类型,值18,二进制 1 1000,右移动一位,做zigzag解压后,得到 1100, 就是十进制赋值的12.
函数funCall的第五个参数:
5:i64 argI64,
字节 16 44 表示ArgI64,
高4为1表示编号偏移1,低4为6表示类型 0x06为64位数值类型,值44,二进制 100 0100,右移动一位,做zigzag解压后,得到 10 0010, 就是十进制赋值的34.
函数funCall的第六个参数:
6:double argDouble,
字节 17 71 3d 0a d7 a3 70 26 40 表示ArgDouble,
高4为1表示编号偏移1,低4为7表示类型 0x07为double数值类型,值71 3d 0a d7 a3 70 26 40,为11.22.
函数funCall的第七个参数:
7:string argString,
字节 18 05 6c 6f 67 69 6e表示ArgString,
高4为1表示编号偏移1,低4位8表示类型 0x08为二进制字符串类型,
05 表示varint后字符串的长度 5,值 6c 6f 67 69 6e为字符串"login"
函数funCall的第八个参数:
8:map<string, string> paramMapStrStr,
字节 1b 02 88 04 6e 61 6d 65 06 6e 61 6d 65 73 73 04 70 61 73 73 05 76 70 61 73 73表示paramMapStrStr,
高4位1表示编号偏移1,低4位b表示类型 0x0b为Map类型,
02 表示varint后Map元素的个数 2,
88 表示Map元素的键和值的类型都为二进制字符串(高4位 8表示键的类型 0x08 为二进制字符串类型,低4位8表示值的类型 0x08 为二进制字符串类型)
Map的第一个键: 04 6e 61 6d 65 为长度为4的字符串 6e 61 6d 65 值 "name"
Map的第一个键的值:06 6e 61 6d 65 73 73 为长度为6的字符串 6e 61 6d 65 73 73值 "namess"
Map的第二个键: 04 70 61 73 73 为长度为4的字符串 70 61 73 73 值 "pass"
Map的第二个键的值:05 76 70 61 73 73 为长度为5的字符串 76 70 61 73 73值 "vpass"
函数funCall的第九个参数:
9:map<i32, string> paramMapI32Str,
字节 1b 02 58 14 05 76 61 6c 31 30 28 05 76 61 6c 32 30表示paramMapI32Str,
高4位1表示编号偏移1,低4位b表示类型 0x0b为Map类型,
02 表示varint后Map元素的个数 2,
58 表示Map元素的键和值的类型都为二进制字符串(高4位 5表示键的类型 0x05 为32位数值类型,低4位8表示值的类型 0x08 为二进制字符串类型)
Map的第一个键: 14,二进制 1 0100,右移动一位,做zigzag解压后,得到 1010, 就是十进制赋值的10.
Map的第一个键的值:05 76 61 6c 31 30为长度为5的字符串 76 61 6c 31 30值 "val10"
Map的第二个键: 28,二进制 101 000,右移动一位,做zigzag解压后,得到 1 0100, 就是十进制赋值的20.
Map的第二个键的值:5 76 61 6c 32 30 为长度为5的字符串 76 70 61 73 73值 "val20"
函数funCall的第十个参数:
10:set<string> paramSetStr,
字节 1a 38 04 65 6c 65 31 04 65 6c 65 32 04 65 6c 65 33表示paramSetStr,
高4位1表示编号偏移1,低4位a表示类型 0x0a为Set类型,
38 表示元素的个数和类型(高4位3表示set有3个元素,低4位8表示值的类型 0x08 为二进制字符串类型)
Set的第一个值: 04 65 6c 65 31,长度为4的字符串65 6c 65 31为"ele1"
Set的第二个值: 04 65 6c 65 32,长度为4的字符串65 6c 65 32为"ele2"
Set的第三个值: 04 65 6c 65 33,长度为4的字符串65 6c 65 33为"ele3"
函数funCall的第十一个参数:
11:set<i64> paramSetI64,
字节 1a 36 16 2c 42表示paramSetI64,
高4位1表示编号偏移1,低4位a表示类型 0x0a为Set类型,
36 表示元素的个数和类型(高4位3表示set有3个元素,低4位6表示值的类型 0x06 为64为数值类型)
Set的第一个值: 16,二进制 10110,右移动一位,做zigzag解压后,得到 1011, 就是十进制赋值的11.
Set的第二个值: 2c,二进制 101100,右移动一位,做zigzag解压后,得到 10110, 就是十进制赋值的22.
Set的第三个值: 42,二进制 1000010,右移动一位,做zigzag解压后,得到 100001, 就是十进制赋值的33.
函数funCall的第十二个参数:
12:list<string> paramListStr,
字节 19 28 03 6c 31 2e 03 6c 32 2e表示paramListStr,
高4位1表示编号偏移1,低4位9表示类型 0x09为List类型,
28 表示元素的个数和类型(高4位3表示set有2个元素,低4位8表示值的类型 0x08 为二进制字符串类型)
List的第一个值: 03 6c 31 2e,长度为3的字符串6c 31 2e为"l1."
List的第二个值: 03 6c 32 2e,长度为3的字符串6c 32 2e为"l2."
最后一个字节 00 表示消息结束。
------------------------------------------------------------------------------------------------------------
开始分析抓包的响应数据。
响应:
0000 82 41 01 07 66 75 6e 43 61 6c 6c 09 00 28 14 72
0010 65 74 75 72 6e 20 31 20 62 79 20 46 75 6e 43 61
0020 6c 6c 2e 14 72 65 74 75 72 6e 20 32 20 62 79 20
0030 46 75 6e 43 61 6c 6c 2e 00
第一个字节 82 表示:COMPACT协议版本。
COMPACT_PROTOCOL_ID = 0x082
第二个字节41表示:消息请求,如何计算得到41呢?
COMPACT_VERSION = 1
COMPACT_VERSION_MASK = 0x1f
COMPACT_TYPE_MASK = 0x0E0
COMPACT_TYPE_BITS = 0x07
COMPACT_TYPE_SHIFT_AMOUNT = 5
(COMPACT_VERSION & COMPACT_VERSION_MASK) | ((byte(typeId) << COMPACT_TYPE_SHIFT_AMOUNT) & COMPACT_TYPE_MASK)
消息请求的message TypeId为1,带入计算
(0x01 & 0x1f) | ((0x02 << 5) & 0xe0
= 0x01 | 0x40 & 0xe0
= 0x01 | 0x40
= 0x41
第三个字节 01 为varint后的流水号 01.
第四个字节 07 为varint后消息的长度 07.
字节 66 75 6e 43 61 6c 6c 为消息名称字符串 funCall
响应参数:
list<string>
字节 09 00 28 14 72 65 74 75 72 6e 20 31 20 62 79 20 46 75 6e 43 61 6c 6c 2e 14 72 65 74 75 72 6e 20 32 20 62 79 20 46 75 6e 43 61 6c 6c 2e
09 表示类型 0x09为List类型,
00 表示响应时字段的编号为0(返回值确实没有编号),由于返回值没有字段编号,所以类型和编号要分开到不同的字节里面。
28 表示元素的个数和类型(高4位3表示set有2个元素,低4位8表示值的类型 0x08 为二进制字符串类型)
List的第一个值: 14 72 65 74 75 72 6e 20 31 20 62 79 20 46 75 6e 43 61 6c 6c 2e,长度为20的字符串72 65 74 75 72 6e 20 31 20 62 79 20 46 75 6e 43 61 6c 6c 2e为"return 1 by FunCall."
List的第二个值: 14 72 65 74 75 72 6e 20 32 20 62 79 20 46 75 6e 43 61 6c 6c 2e,长度为20的字符串72 65 74 75 72 6e 20 32 20 62 79 20 46 75 6e 43 61 6c 6c 2e为"return 2 by FunCall."
最后一个字节00表示响应消息结束。
Done.