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.