天天看點

Protocol Buffer 分析

​ 谷歌的protocol buffer以其輕量級和跨平台的性質,成為了遊戲中資料傳輸的首選。序列化的過程可以參考該文章 ,還有官方的解釋https://developers.google.com/protocol-buffers/docs/encoding。

一、反序列化pb資料可行性分析

​ 傳統的pb序列化與反序列化:

序列化:	協定檔案(.proto)	+	原始資料		==>		序列化後資料
反序列化:	序列化後資料		+	協定檔案(.proto)		==> 	原始資料
           

​ 以傳統方式,需要.proto檔案才能反序列化出原始資料。但是通過了解序列化的過程,可以知道其實序列化時是按照下列對應類型序列化資料的,那麼如果對每個type都能有對應的方法反序列化出原始資料,那麼就能夠反序列化出整個原始資料。

Type Meaning Used For
Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimit string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float
二、反序列化pb資料例子分析

​ pb的序列化方式采用了如下方式,K-V

KEY-VALUE

或 K-L-V

KEY-LENGHT-VALUE

,好像也有叫 T-V

TAG-VALUE

和 T-L-V

TAG-LENGTH-VALUE

Protocol Buffer 分析

Key 的定義如下:

(field_number << 3) | wire_type
           

其中field_nuber 代表pb中的第幾個資料,wire_type

從上面的表格可以知道其數值在0-5區間内

代表該資料的類型。

Intel CPU的架構是小端模式 ,最低位元組在前(Small-Endian)

下面是某個pb資料反序列化分析過程,這個分析過程中需要先了解Varint編碼方式,UTF8編碼方式:

  • Varint編碼方式

Protocol Buffer 分析
  • UTF8編碼方式
    Unicode/UCS-4 bit數 UTF-8 byte數 備注
    0000 ~007F 0~7 0XXX XXXX 1
    0080 ~07FF 8~11 110X XXXX10XX XXXX 2
    0800 ~FFFF 12~16 1110XXXX10XX XXXX10XX XXXX 3 基本定義範圍:0~FFFF
    1 0000 ~1F FFFF 17~21 1111 0XXX10XX XXXX10XX XXXX10XX XXXX 4 Unicode6.1定義範圍:0~10 FFFF
    20 0000 ~3FF FFFF 22~26 1111 10XX10XX XXXX10XX XXXX10XX XXXX10XX XXXX 5 說明:此非unicode編碼範圍,屬于UCS-4 編碼早期的規範UTF-8可以到達6位元組序列,可以覆寫到31位元(通用字元集原來的極限)。盡管如此,2003年11月UTF-8 被 RFC 3629 重新規範,隻能使用原來Unicode定義的區域, U+0000到U+10FFFF。根據規範,這些位元組值将無法出現在合法 UTF-8序列中
    400 0000 ~7FFF FFFF 27~31 1111 110X10XX XXXX10XX XXXX10XX XXXX10XX XXXX10XX XXXX
  • 分析過程
45 00 1A 0F 38 35 37 35 34 32 30 35 32 39 2E 33 34 39 36 22 0C E5 B9 BF E4 B8 9C E6 B7 B1 E5 9C B3 08 CB 8A 01 12 20 36 38 61 31 35 36 66 62 38 66 34 65 64 38 31 63 65 61 31 32 37 37 37 61 34 34 30 32 64 38 34 35

45 00 協定長度(小端形式)           00 45 --> 69個位元組

1A --> 00011010
3 << 3 | 2	field_number為3,wire_type為2
0F --> 00001111 該值是15個位元組
38 --> 00111000 : 56 字元8的char是56	*(這裡參與了UTF8來解析,有可能是嵌套,後面分析讨論)*
35 --> 00110101 : 53 字元5
37 --> 00110111 : 55 字元7
35 --> 00110101 : 53 字元5
34 --> 00110100 : 52 字元4
32 --> 00110010 : 50 字元2
30 --> 00110000 : 48 字元0
35 --> 00110101 : 53 字元5
32 --> 00110010 : 50 字元2
39 --> 00111001 : 57 字元9
2E --> 00101110 : 46 字元.
33 --> 00110011 : 51 字元3
34 --> 00110100 : 52 字元4
39 --> 00111001 : 57 字元9
36 --> 00110110 : 54 字元6
pb中第3個的值是:8575420529.3496

22 --> 00100010
4 << 3 | 2 field_number為4,wire_type為2
0C --> 00001100 該值是12位元組
E5 --> 11100101 (UTF格式,表明三個位元組一起)
B9 --> 10111001 
BF --> 10111111
E5 B9 BF --> 01011110 01111111 --> 24191 字元'廣'
E4 --> 11100100 (UTF格式,表明三個位元組一起)
B8 --> 10111000
9C --> 10011100
E4 B8 9C --> 01001110 00011100 --> 19996 字元'東'
E6 --> 11100110 (UTF格式,表明三個位元組一起)
B7 --> 10110111
B1 --> 10110001
E6 B7 B1 --> 01101101 11110001 --> 28145 字元'深'
E5 --> 11100101 (UTF格式,表明三個位元組一起)
9C --> 10011100
B3 --> 10110011
E5 9C B3 --> 01010111 00110011 --> 22323 字元'圳'
pb中第4個的值是:廣東深圳

08 --> 00001000
1 << 3 | 0 field_number為1,wire_type為0
CB --> 11001011 (Varint 高位為1表示下個位元組是一起的)
8A --> 10001010 (Varint 高位為1表示下個位元組是一起的)
01 --> 00000001 
CB 8A 01 --> (PB采用小端)0000001 0001010 1001011 --> 17739
pb中第1個的值是:17739

12 --> 00010010
2 << 3 | 2 field_number為2,wire_type為2
20 --> 00100000 該值是32位元組
36 --> 00110110 : 54 字元6
38 --> 00111000 : 56 字元8
61 --> 01100001 : 97 字元a
31 --> 00110001 : 49 字元1
35 --> 00110101 : 53 字元5
36 --> 00110110 : 54 字元6
66 --> 01100110 : 102 字元f
62 --> 01100010 : 98 字元b
38 --> 00111000 : 56 字元8
66 --> 01100110 : 102 字元f
34 --> 00110100 : 52 字元4
65 --> 01100101 : 101 字元e
64 --> 01100100 : 100 字元d
38 --> 00111000 : 56 字元8
31 --> 00110001 : 49 字元1
63 --> 01100011 : 99 字元c
65 --> 01100101 : 101 字元e
61 --> 01100001 : 97 字元a
31 --> 00110001 : 49 字元1
32 --> 00110010 : 50 字元2
37 --> 00110111 : 55 字元7
37 --> 00110111 : 55 字元7
37 --> 00110111 : 55 字元7
61 --> 01100001 : 97 字元a
34 --> 00110100 : 52 字元4
34 --> 00110100 : 52 字元4
30 --> 00110000 : 48 字元0
32 --> 00110010 : 50 字元2
64 --> 01100100 : 100 字元d
38 --> 00111000 : 56 字元8
34 --> 00110100 : 52 字元4
35 --> 00110101 : 53 字元5
pb中第2個的值是:68a156fb8f4ed81cea12777a4402d845

整個pb解析完畢
協定 1001
pb内容:
{
    17739,
    68a156fb8f4ed81cea12777a4402d845,
    8575420529.3496,
    廣東深圳
}

該協定.proto如下:
message msg_login_req
{
	optional uint32 uid  		= 1;		// 帳号ID
	optional bytes  key  		= 2;		// 密碼		
	optional bytes  deviceid	= 3;		// 裝置ID
	optional bytes  city		= 4;		// 城市
}
           
  • 總結
    1. 序列化後的pb資料能夠在沒有.proto檔案下反序列化出原始資料,但是因為缺少.proto,無法得知每個資料對應的key。
    2. 反序列化過程中遇到了wire_type為2的情況下,直接當成了UTF8形式的資料進行解碼,但是wire_type為2,可能是嵌套資料。
三、抓取pb資料

​ Fildder可以抓取HTTP或HTTPS包,但無法抓取TCP包,需要用到另一款工具PacketCapture。安卓下載下傳位址

Protocol Buffer 分析

Packet Capture抓取資料步驟:

  1. 關掉其他程序,打開Packet Caputre
  2. 打開要抓取的遊戲
  3. 将抓取的内容儲存為字尾名為.pcap的檔案
  4. 用Wireshark分析.pcap檔案内容
Protocol Buffer 分析

反序列化結果:

Protocol Buffer 分析
四、編寫反序列化工具

​ 因為工作中用的是Lua語言,是以這個反序列化工具也想使用Lua語言來編寫。可以使用srLua将編寫的lua檔案編譯成

.exe

檔案。srLua 官方下載下傳位址。

建議使用支援高版本Lua的srLua,例如在Lua5.1中要使用位操作,需要自己實作/使用quick-cocos2d中的bitExtend.lua/使用LuaBit。而在Lua5.3中是支援位運算的。
  • 例如 test.lua是反序列化的代碼檔案,将 test.lua編譯成 test.exe
Protocol Buffer 分析
  • 編寫test.lua檔案
    1. 反序列化Varint類型

      根據Varint的編碼方式,每次取一個位元組,對該位元組的最高位進行判斷,如果為1,則繼續取下一個位元組,直到最高位為0。将該過程中所有的位元組去掉最高位,按倒序排列。

      十六進制位元組 CB 8A 01
      轉為二進制 11001011 10001010 00000001
      去掉最高位 x1001011 x0001010 x0000001
      倒序排列 0000001 0001010 1001011
      整合 0000001 0001010 1001011
      結果 17739
      --[[
      	擷取varint類型資料
      	pb中wire_type為0(擷取key也使用該方式)
      --]]
      function DecodePBData:getVarintData(content,startIndex,tag)
      	if not content then return end
      	local varintTable = {}
      	local strData = ""
      	local varintDataLen = 0
      	while true do
      		if startIndex+varintDataLen+1 > #content then return end
      		local str = string.sub(content,startIndex+varintDataLen,startIndex+varintDataLen+1) 
      		varintDataLen = varintDataLen + 2 + space_byte
      		local d_num = tonumber(str,16)
      		if not d_num then return end
      
      		local hightBit = d_num & (1 << 7)
      		local byte_num = d_num & ((1 << 7) - 1)
      		table.insert(varintTable,byte_num)
      		strData = strData .. str
      		-- 最高位為0,表明結束
      		if hightBit == 0 then
      			break
      		end
      	end
      
      	local varint_num = 0
      	for i = 1, #varintTable do
      		varint_num = varint_num + (varintTable[i] << ((i - 1) * 7))
      	end
      
      	tag = tag or ""
      	output.d("[".. tag .. "getVarintData] strData::" .. strData)
      	output.d("[".. tag .. "getVarintData] varint_num::" .. varint_num)
      	return varint_num, varintDataLen
      end
                 

      未解決的點:

      sint32,sint64

      sint32,sint64是相容的,他們都是使用VARINTS(Zigzag(v))進行編碼的。但和其他使用VARINTS(v)進行編碼的無法區分

      bool值與數字0,1

      反序列化後,0,1無法判斷是數字0,1或是bool值false,true

    2. 反序列化64-bit類型

      64-bit即8位元組,該類型固定取8位元組進行反序列化。

      fixed64,無符号64位資料,取出該資料(小端),對每位元組資料進行相應的左移操作,然後将左移的資料進行相加即可。

      function DecodePBData:getFixed64(content,startIndex)
      	if not content then return end
      	local intData = 0
      	local strData = ""
      	local intDataLen = 0
      	for i = 1, 8 do
      		local str = string.sub(content,startIndex+intDataLen,startIndex+intDataLen+1)
      		intDataLen = intDataLen + 2 + space_byte
      		local d_num = tonumber(str,16)
      		intData = intData + (d_num << ((i-1) * 8))
      		strData = strData .. str
      	end
      
      	output.d("[getFixed64] strData:" ..  strData)
      	output.d("[getFixed64] int_num:" .. intData)
      	return intData, intDataLen
      end
      
                 
      sfixed64,有符号64位資料,方法與fixed64相似,隻是增加判斷最高位(符号位),如果是負數(負數在計算機在是以補碼的形式存儲的),先

      -1

      求其反碼,再異或操作

      ~0xFFFFFFFFFFFFFFFF

      求其原碼,再增加符号。

      -((data - 1) ~ 0xFFFFFFFFFFFFFFFF)

      double,雙精度浮點數。1位符号位,11位整數位,52位小數位
      --[[
      	讀double
      	bytes 8位元組byte數組
      --]]
      function DecodePBData:getDouble(bytes)
      	if not bytes or #bytes < 8 then return end
      	local sign = 1
      	local mantissa = bytes[2] % 2^4
      	for i = 3, 8 do
      		mantissa = mantissa * 256 + bytes[i]
      	end
      	if bytes[1] > 127 then sign = -1 end
      	local exponent = (bytes[1] % 128) * 2^4 + math.floor(bytes[2] / 2^4)
      
      	if exponent == 0 then
      		return 0
      	end
      	mantissa = (math.ldexp(mantissa, -52) + 1) * sign
      	return math.ldexp(mantissa, exponent - 1023)
      end
                 

      未解決的點:

      雖然對于fixed64,sfixed64,double都能夠單獨反序列化。但是對于得到的序列化資料,卻無法判斷是對應fixed64,sfixed64,double中的哪種類型。

    3. 反序列化Length-delimit類型

      length-delimit中有string, bytes, embedded messages, packed repeated fields,而這幾個可以分成幾大類。

      • string,bytes UTF8編碼類
      • embedded messages,packet repeated fields(沒有帶【packed = true】)嵌套類
      • packet repeated fields (帶【packed = true】)K-L-多個V類型
      UTF8編碼類:
      --[[
      	十六進制資料轉UTF8
      	hexData 十六進制資料
      --]]
      function DecodePBData:hexToUtf8(hexData)
      	if not hexData then return end
      	output.d("[hexToUtf8] str:" .. hexData)
      	-- 擷取單個位元組
      	local hexIndex = 1
      	local function getSingleByte()
      		if hexIndex+1 > #hexData then return end
      		local str = string.sub(hexData,hexIndex,hexIndex+1)
      		hexIndex = hexIndex + 2 + space_byte
      		local utf8_num = tonumber(str,16)
      		return utf8_num, str
      	end
      
      	local _utf8Str = ""
      	local singleCharByte = 0
      	local tmpStr = ""
      	while true do
      		local utf8_num, utf8_str = getSingleByte()
      		if not utf8_num then break end
      		tmpStr = tmpStr .. utf8_str
      		-- 偏移位
      		local offsetBit = 7
      		while offsetBit > 0 do
      			local bitNum = utf8_num & (1 << offsetBit)
      			if bitNum == 0 then
      				break
      			end
      			offsetBit = offsetBit - 1
      		end
      		-- utf8字元占用位元組數
      		local utf8ByteCount = offsetBit == 7 and 1 or 7 - offsetBit
      		-- 第一個位元組對應的真實數字
      		singleCharByte = singleCharByte + (utf8_num & ((1 << (8-utf8ByteCount)) - 1)) << (((utf8ByteCount-1) * 6))
      		if utf8ByteCount > 1 then
      			-- 兩個位元組以上的
      			for i = 2, utf8ByteCount do
      				local other_utf8_num,other_utf8_str = getSingleByte()
      				singleCharByte = singleCharByte + ((other_utf8_num & 0x3F) << ((utf8ByteCount-i) * 6))
      				tmpStr = tmpStr .. other_utf8_str
      			end
      		end
      
      		output.d("[hexToUtf8] sigle_utf8_str::" .. tmpStr)
      		output.d("[hexToUtf8] sigle_utf8_char::" .. singleCharByte)
      		_utf8Str = _utf8Str .. utf8.char(singleCharByte)
      		singleCharByte = 0
      		tmpStr = ""
      	end
      
      	output.d("[hexToUtf8] _utf8Str::" .. _utf8Str)
      	return _utf8Str
      end
                 

      嵌套類:

      ​ 對于嵌套類,隻要将其當做是一個全新的pb序列化資料,進行反序列即可。工具中調用decodeMessage(解碼單個協定體資料) 方法即可。

      K-L-多個V類型:

      ​ 對于多個V類型,是以相同的wire_type進行解析。可以發現對于UTF8類型,不适用多個V,因為對于UTF8類型沒有固定的分界。

      --[[
      	擷取packet repeated fields [packed = true]資料 (用于value是varint資料)
      	packet repeated fields可當做是嵌套,調用decodeMessage即可
      --]]
      function DecodePBData:getPackedRepeatedFields(content,startIndex,wire_type)
      	if not content then return end
      	local repeatedPacket = {}
      	while true do
      		if startIndex > #content then break end
      		if wire_type == 0 then
      			-- varint
      			local varint_num, varint_len = DecodePBData:getVarintData(content,startIndex)
      			table.insert(repeatedPacket, varint_num)
      			startIndex = startIndex + varint_len
      		elseif wire_type == 1 then
      			-- 64-bit
      
      		elseif wire_type == 2 then
      			-- length-delimit
      
      		elseif wire_type == 5 then
      			-- 32-bit
      
      		else
      			break
      		end
      	end
      
      	output.d("[getPackedRepeatedFields] wire_type: " .. tostring(wire_type))
      	output.d("[getPackedRepeatedFields] repeatedPacket: " .. table.concat(repeatedPacket,","))
      	return repeatedPacket
      end
                 

      未解決的點:

      ​ 判斷又K-L-多個V類型解析,但是卻無法得知wire_type類型

      需要優化的點:

      ​ 現在對于UTF8,嵌套類,K-L-多個V類型的區分,是先判斷是否是嵌套類

      先模拟嵌套解析,如果能夠解析成功,則表明是嵌套類

      ,否則判斷是否是UTF8格式,如果是則用UTF8方法解析,否則用K-L-多個V類型解析
    4. 反序列化32-bit類型

      32-bit即4位元組,該類型固定取4位元組進行反序列化。

      fixed32,無符号32位資料,取出該資料(小端),對每位元組資料進行相應的左移操作,然後将左移的資料進行相加即可。

      --[[
      	讀取固定的4位元組資料
      --]]
      function DecodePBData:getFixed32(content,startIndex)
      	if not content then return end
      	local intData = 0
      	local strData = ""
      	local intDataLen = 0
      	for i = 1, 4 do
      		local str = string.sub(content,startIndex+intDataLen,startIndex+intDataLen+1)
      		intDataLen = intDataLen + 2 + space_byte
      		local d_num = tonumber(str,16)
      		intData = intData + (d_num << ((i-1) * 8))
      		strData = strData .. str
      	end
      
      	output.d("[getFixed32] strData:" ..  strData)
      	output.d("[getFixed32] int_num:" .. intData)
      	return intData, intDataLen
      end
                 
      sfixed32,有符号32位資料,方法與fixed32相似,隻是增加判斷最高位(符号位),如果是負數(負數在計算機在是以補碼的形式存儲的),先

      -1

      求其反碼,再異或操作

      ~0xFFFFFFFF

      求其原碼,再增加符号。

      -((data - 1) ~ 0xFFFFFFFF)

      float,雙精度浮點數。1位符号位,8位整數位,23位小數位
      --[[
      	讀float
      	bytes 4位元組byte數組
      --]]
      function DecodePBData:getFloat(bytes)
      	if not bytes or #bytes < 4 then return end
      	local sign = 1
      	local mantissa = bytes[2] % 2 ^ 7
      	for i = 3, 4 do
      		mantissa = mantissa * 256 + bytes[i]
      	end
      	if bytes[1] > 127 then sign = -1 end
      	local exponent = (bytes[1] % 128) * 2 + math.floor(bytes[2] / 2 ^ 7)
      
      	if exponent == 0 then
      		return 0
      	end
      	mantissa = (math.ldexp(mantissa, -23) + 1) * sign
      	return math.ldexp(mantissa, exponent - 127)
      end
                 

      未解決的點:

      雖然對于fixed32,sfixed32,float都能夠單獨反序列化。但是對于得到的序列化資料,卻無法判斷是對應fixed32,sfixed32,float中的哪種類型。

    5. 日志處理

      在早期做解析檔案時,需要對每個方法進行測試,了解各個方法解析中的過程,需要較多的日志。後期完成時,隻需幾個重要的資訊日志,模拟了Android中的日志分級列印,加了日志。

      Protocol Buffer 分析
  • 驗證test.lua

    在接收到socket資料時,調用編譯好的test.exe

    Protocol Buffer 分析
    在遊戲運作過程中,對于每個協定的資料,都會調用test.exe進行解析,運作了一段時間後,解析的結果如下:[test.exe解析結果](file://F:\資料\pb分析\遊戲中運作一段時間後解析結果.txt)
五、總結

​ 雖然現在的插件已經能夠适用于工作中的遊戲,但在編寫test.lua解析pb插件時,可以看出還有一些點沒有解決。下一版本的解析插件可以從以下幾個點進行優化:

  1. 對這一版本中未解決的點或需要優化的點就行優化。
  2. 整體的代碼可以優化下。
  3. 對apk進行解包,考慮如何運用解包後的xxx.pb檔案。

繼續閱讀