0x01 protobuf的基本概念
protobuf通过定义".proto"文件来描述数据的结构。.proto文件中用"Message"所表示所需要序列化的数据的格式。Message由field组成,Field类似JAVA或者C++中成员变量,通常一个field的定义包含修饰符、类型、名称和ID。下面看一个简单的.proto文件的例子:
package testInfo;
message Chats
{optional string content1 = 1;required int32 content2 = 2;message EmbMsg{optional string a = 1;}repeated EmbMsg a = 3;
}
然后利用protoc工具生成.h和.cpp文件,并且编写代码
testInfo::Chats chatTest;chatTest.set_content1("hello");chatTest.set_content2(32);testInfo::Chats::EmbMsg *pMsg = chatTest.add_a();pMsg->set_a("aaaa");pMsg = chatTest.add_a();pMsg->set_a("bbb");std::string outputStr;chatTest.SerializeToString(&outputStr);printf("str %s", outputStr.c_str());
得到十六进制流数据为:
0x00DE94C8 0a 05 68 65 6c 6c 6f 10 20 1a 06 0a 04 61 61 61 ..hello. ....aaa
0x00DE94D8 61 1a 05 0a 03 62 62 62
0x02 protobuf流的反解析
2.1 Varint编码
Protobuf的二进制使用Varint编码。Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。
例如:十六进制流里面其中两个字节:0x95 0x01,则其转换运算为:(0x95 & 0x7F) | (0x01 << 0x7) = 0x5 | 0x80 = 0x95。
若其中四个字节:0x9D 0xF4 0xC1 0xCB 0x05,则其转换运算为:
(0x9D & 0x7F) | (0xF4 & 0x7F)<<7 | (0xC1 & 0x7F)<<E | (0xCB & 0x7F)<<0x15 | 05<<0x1C
= 1D | 3A00 | 104000 | 9600000 | 50000000
= 59707A1D
2.2 数值类型
Protobuf经序列化后以二进制数据流形式存储,这个数据流是一系列key-Value对。Key用来标识具体的Field,在解包的时候,Protobuf根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 Field。
Key 的定义如下:
(field_number << 3) | wire_type
Key由两部分组成。第一部分是 field_number,比如消息chatTest.content1中 的 field_number 为 1。第二部分为 wire_type。表示 Value 的传输类型。Wire Type 可能的类型如下表所示:
type | Meaning | Used For |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
以上面生成的十六进制流我们可以开始分析
required和optional不会有任何字节来表示这个修饰符。
repeated会存在相同的field_number。
0a 05
0A -> field_num=1, type=2;
05 -> 代表字符串长度05
68 65 6c 6c 6f
-> "hello"
10 20
10->field_num=2, type=0;
20->value=0x20;
1a 06 0a 04 61 61 61 61 1a 05 0a 03 62 62 62
1a->field_num=3, type=2;
06->结构体长度06
0a->field_num=1,type=2;
04->字符串长度04
61 61 61 61 ->value="aaaa"
1a->field_num=3, type=2;
05->结构体长度05
0a->field_num=1,type=2;
03->字符串长度03
61 61 61 61 ->value="bbb"
2.3 protoc 进行反序列化
上面的步骤是手动解析的过程,而利用google提供的工具可以帮助我们自动化的解析以上过程,在面对复杂的protobuf结构的时候能达到事半功倍的效果。按下面步骤来做:
首先配置java环境
其次安装jython,这里
然后编写python脚本,protobuf.py
import subprocess
def decode(data):process = subprocess.Popen([r'D:\protobuf\protoc.exe', '--decode_raw'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)output = error = Nonetry:output, error = process.communicate(data)except OSError:passfinally:if process.poll() != 0:process.wait()return outputf = open(r"D:\testprotobuf.bin", "rb")
data = f.read()
print 'data:/n',decode(data)
f.close()
其中testprotobuf.bin是我们的protobuf流文件。
最后运行脚本
C:\Users\Administrator>cd C:\jython2.7.0C:\jython2.7.0>java -jar jython.jar D:\task\qq-ups\protobuf.py
data:/n 1: "hello"
2: 32
3 {1: "aaaa"
}
3 {1: "bbb"
}
利用反序列化的结构来推测.proto的message的结构及每个字段的含义,就能达到protobuf流反解析的目的了。