近期公司项目使用到了protobuff。虽然之前对protobuff就一直有耳闻,但是对传闻其高效便捷并不是很理解,直到此项目才对protobuff有了一些较为深刻的认识。
先说下protobuff的用法 以c++为示例。 关于如何下载 编译就不多说了。先生成xx.proto文件, 里面 按照格式写入你的数据结构 例如
message Data
{
required string id = 1;
optional int32 data1 = 2;
repeated int32 data2 = 3;
}
用 protoc.exe --cpp_out=*.proto 生成 相应的cpp 和h文件
这里有3个字段 第一个 required 表示此字段必须赋值 如果没赋值会怎样? 其实也不会怎样生成出来的代码中 IsInitialized 会返回false。optional 表示可有可无的字段。
repeated 表示可能会重复的字段 其在代码的表现形式则是一个动态数组。关于数据类型 如下
Type | Meaning | Used For |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
对应枚举 enum WireType {
WIRETYPE_VARINT = 0,
WIRETYPE_FIXED64 = 1,
WIRETYPE_LENGTH_DELIMITED = 2,
WIRETYPE_START_GROUP = 3,
WIRETYPE_END_GROUP = 4,
WIRETYPE_FIXED32 = 5,
};
当生成cpp文件时 会将你所定义的所有变量都生成在类里 如以上生成的数据 则有如下规则
Data a; 设置某值有 a.set_id(“1”); 这里的set_id 表示对 id字段赋值 获得id则有 a.id(); 所有字段的设置和获取都雷同此方法。
那么接下来说下protobuff的打包规则,它使用的是Tag-Value模式 也就是说一个字段由2个数据表示打包出来的最后二进制数据如下 [Tag][Value][Tag][Value][Tag][Value]....... 这样做的好处是如果不用打包的数据不出现在里面即可节省了很多空间。这里要说一下protobuff使用的字段按字节来说每个字节的第1位表示后面的一个字节是否和此是关联的,也就是说如果某个字节的第一位如果是1 而后面一个字节第一位是0 那么表示这2个字节组成一个数 ,如果第二个字节的第一位也是1 而第3个字节的第1位是0 那么表示这3个字节组成一个数以此类推,所有字节的第一位都是会被protobuf使用的,除了字符串类型,字符串类型仅仅是长度+memcpy。这样做从概率学上将会在一定程度上压缩空间,因为如果是一个数值很小的数 正常的int 需要4个字节 而此时可能仅需要1个字节当然我们不能将Tag的大小忽略那么Tag最小也是1字节这样一个小数据将占用2字节就能表示。这里要注意的是一个32位 4字节的字段 protobuf是技能使用的只有28位。Tag的制作规则 ,按字节来说后面三位用来表示数据类型 也就是WireType, 前面的数字则是其字段在定义时的位置 例如 id = 1 这个1 就表示此值 。当然 这个规则也遵循7位规则,也就是一个字节的最高位会被protobuf保留使用,这样Tag能用的数值为 28 - 3=25 位 这个数字也是天文数字了。而其中当数据是重复数据 或者字符串时 会在Tag后先打包一个长度(数组长度或者字符串长度)再将数据依次打包进来。字符串比较简单memcpy到相关内存即可
数组的话则是用长度计数,某一数据是否结束用其第一位是否为0表示
这样的打包方式还有一个好处就是便于扩展 ,比如一个数据以protobuf形式存储在数据库 ,而之后功能扩展了 比如有如下格式
message Data
{
required string id = 1;
optional int32 data1 = 2;
repeated int32 data2 = 3;
optional int32 data3 = 4
}