Thrift的TCompactProtocol緊湊型二進制協議分析

Thrift的緊湊型傳輸協議分析:git

用一張圖說明一下Thrift的TCompactProtocol中各個數據類型是怎麼表示的。算法

 

報文格式編碼:apache

bool類型:app

  一個字節。socket

  若是bool型的字段是結構體或消息的成員字段而且有編號,一個字節的高4位表示字段編號,低4位表示bool的值(0001:true, 0010:false),即:一個字節的低4位的值(true:1,false:2).函數

  若是bool型的字段單獨存在,一個字節表示值,即:一個字節的值(true:1,false:2).測試

Byte型:ui

  一個字節的編號與類型組合(高4位編號偏移1,低4位類型),一個字節的值.this

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.

相關文章
相關標籤/搜索