protobuf的安装
tar zxvf protobuf-cpp-3.8.0.tar.gz
cd protobuf-3.8.0/
./configure CXXFLAGS="-O2" CFLAGS="-O2"
make
sudo make install
sudo ldconfig
protoc --version
protobuf编译脚本Makefile
pb_file:IM.BaseDefine.proto IM.Login.proto
protoc --proto_path=./ --cpp_out=./ *.proto
# a.out is one exe
a.out:main.cc pb_file
g++ main.cc base.pb.cc -I $INCLUDE_PATH -L $LIB_PATH -lprotobuf -pthread
clean:
rm a.out *.pb.*
proto文件的定义
//IM.BaseDefine.proto
syntax = "proto3";
package IM.BaseDefine; //服务前缀,包名,防止冲突
option optimize_for = LITE_RUNTIME;
enum PhoneType{
PHONE_DEFAULT = 0x0;
PHONE_HOME = 0x0001; // 家庭电话
PHONE_WORK = 0x0002; // 工作电话
}
//IM.Login.proto
syntax = "proto3";
package IM.Login; //服务前缀,包名,防止冲突
import "IM.BaseDefine.proto"; //这里包含了别的proto文件
option optimize_for = LITE_RUNTIME; //optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME
//缺省情况下是SPEED。
//SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
//CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。
//LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。 这是以牺牲Protocol Buffer提供的反射功能为代价的
message Phone{
string number = 1;
IM.BaseDefine.PhoneType phone_type = 2;
}
message Book{
string name = 1;
float price = 2;
}
message Person{
string name = 1;
int32 age = 2;
repeated string languages = 3;
Phone phone = 4;
repeated Book books = 5;
bool vip = 6;
string address = 7;
}
//使用T开头测试
message TInt32{
int32 int1 = 1;
}
message TString{
string str1 = 1;
}
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/wait.h>
//依赖proto生成的文件进行使用
#include "IM.BaseDefine.pb.h"
#include "IM.Login.pb.h"
//按结构进行数据构造,并获得序列化后的数据 并输出
bool create_encode_data(std::string &strProto); //参数传出结果
//根据获取到的序列化数据,进行反序列化获取原特定结构的数据
bool decode_data_get_data(std::string &strProto); //传入参数
int main()
{
std::string strProto;
//构造原始数据 获取序列化的数据
create_encode_data(strProto);
//根据序列化的数据 反序列化后打印原始结构数据
decode_data_get_data(strProto);
return 0;
}
//根据proto文件中的结构定义,构造数据
//最终的数据即是序列化后的数据 进行分析
//传出序列化后的数据,这里仅是测试
bool create_encode_data(std::string &strProto)
{
/************************************
message Person{
string name = 1;
int32 age = 2;
repeated string languages = 3;
Phone phone = 4;
repeated Book books = 5;
bool vip = 6;
string address = 7;
}
************************************/
IM::Login::Person person;
person.set_name("my name test"); // 设置以set_为前缀
person.set_age(21);
//repeated字段可以有多个value值
person.add_languages("C++"); // 数组add
person.add_languages("Java");
//取其中的子元素进行数据构造
//mutable_ 嵌套对象时使用,并且是单个对象时使用
IM::Login::Phone *phone = person.mutable_phone();
if(!phone)
{
std::cout << "mutable_phone failed." << std::endl;
return false;
}
phone->set_number("137 7777 9899"); //字符串
phone->set_phone_type(IM::BaseDefine::PHONE_HOME);
//add_针对 repeated多个对象使用,每次增加一个,可以增加多个
//添加第一个对象
IM::Login::Book *book = person.add_books();
book->set_name("c++ plus");
book->set_price(6.7);
//添加第二个对象
book = person.add_books();
book->set_name("Advanced Programming in the UNIX Environment");
book->set_price(16.7);
person.set_vip(true);
person.set_address("xxxx xxx xx");
//这里就已经构成了一个person的对象
//可以对该数据进行序列化,用于网络传输等业务处理
uint32_t buff_size = person.ByteSize();
//这里使用std::string 直接存储
strProto.clear();
strProto.resize(buff_size);
//拷贝序列化后的内容进存储空间 实际就是写入strProto 中
uint8_t * c_protobuf = (uint8_t*)strProto.c_str();
if(!person.SerializeToArray(c_protobuf, buff_size))
{
std::cout<<"proto buff to array error"<<std::endl;
return false;
}
//输出序列化后的数据
printf("序列化后的数据:\n");
for(int i=0;i <buff_size; i++)
{
printf("%02x", c_protobuf[i]);
}
printf("\n");
// for(int i=0;i <buff_size; i++)
// {
// printf("%c", c_protobuf[i]);
// }
// printf("\n");
return true;
}
bool decode_data_get_data(std::string &strProto)
{
//对数据进行反序列化(解析),获取原结构数据(使用)
//调用接口,进行解析,获取到对象结构
IM::Login::Person person;
//strProto 这里使用可能有点不可靠,最好长度传进来
person.ParseFromArray(strProto.c_str(), strProto.size());
//根据IM::Login::Person 结构对内存进行输出
printf("struct data is: \n");
std::cout << " name:\t" << person.name() << std::endl;
std::cout << " age:\t" << person.age() << std::endl;
std::string languages;
for (int i = 0; i < person.languages_size(); i++)
{
if (i != 0)
{
languages += ", ";
}
languages += person.languages(i);
}
std::cout << " languages:\t" << languages << std::endl;
// 自定义message的嵌套并且不是设置为repeated则有has_
if (person.has_phone())
{
const IM::Login::Phone &phone = person.phone();
std::cout << " phone number:\t" << phone.number() << ", type:\t" << phone.phone_type() << std::endl;
}
else
{
std::cout << " no phone" << std::endl;
}
//多个元素 依次取数据
for (int i = 0; i < person.books_size(); i++)
{
const IM::Login::Book &book = person.books(i);
std::cout << " book name:\t" << book.name() << ", price:\t" << book.price() << std::endl;
}
std::cout << " vip:\t" << person.vip() << std::endl;
std::cout << " address:\t" << person.address() << std::endl;
return false;
}
/****************************************
//编译命令
g++ -o test test.cpp IM.BaseDefine.pb.cc IM.Login.pb.cc -lprotobuf -lpthread -std=c++11
//如果编译有报错,请注意环境是否有以前装过的protobuf
序列化后的数据:
0a0c6d79206e616d65207465737410151a03432b2b1a044a61766122110a0d3133372037373737203938393910012a0f0a08632b2b20706c7573156666d6402a330a2c416476616e6365642050726f6772616d6d696e6720696e2074686520554e495820456e7669726f6e6d656e74159a99854130013a0b7878787820787878207878
struct data is:
name: my name test
age: 21
languages: C++, Java
phone number: 137 7777 9899, type: 1
book name: c++ plus, price: 6.7
book name: Advanced Programming in the UNIX Environment, price: 16.7
vip: 1
address: xxxx xxx xx
1、序列化与反序列化
序列化:指将结构化的数据按一定的编码规范转成指定格式的过程;
反序列化:指将转成指定格式的数据解析成原始的结构化数据的过程;
举个例子:Person是一个表示人的对象类型,person是一个Person类型的对象,将person存到一个对应的XML文档中的过程就是一种序列化,而解析XML生成对应Person类型对象person的过程,就是一个反序列化的过程。在这里结构化数据指的就是Person类型的数据,一定的编码规范指的就是XML文档的规范。XML是一种简单的序列化方式,用XML序列化的好处是,XML的通用性比较好,另外,XML是一种文本格式,对人阅读比较友好,但是XML方式比较占空间,效率也不是很高。通常,比较高效的序列化都是采用二进制方式的;将要序列化的结构化数据,按一定的编码规范,转成为一串二进制的字节流存储下来,需要用的时候再从这串二进制的字节流中反序列化出对应的结构化的数据。
2、TLV编码格式:即Tag-Length-Value(其中Length可选)的编码格式。
每个字段都使用TLV的方式进行序列化,一个消息就可以看成是多个字段的TLV序列拼接成的一个二进制字节流。其实这种方式很像Key-Value的方式,所以Tag一般也可以看做Key。显然,这种方式组织的数据并不需要额外的分隔符来划分数据,所以序列化的效率非常高(空间效率)。
编码解析
0a0c6d79206e616d65207465737410151a03432b2b1a044a61766122110a0d3133372037373737203938393910012a0f0a08632b2b20706c7573156666d6402a330a2c416476616e6365642050726f6772616d6d696e6720696e2074686520554e495820456e7669726f6e6d656e74159a99854130013a0b7878787820787878207878
22240a18747970652e676f6f676c65617069732e636f6d2f4661636512080a066c766c766c76
Tag 字段序号 write_type
0a 0c 6d7920 6e616d 652074 657374 //"my name test" 0a 00001 010
10 15 //21 10 00010 000
1a 03 432b2b //"C++" 1a 00011 010
1a 04 4a617661 //"Java"
22 22 00100 010
Length
11 0x11= 17
0a 0a 00001 010
Length
0d 0d
31333720373737372039383939 //"137 7777 9899"
10 01 //1(enum) 10 00010 000
2a 2a 00101 010
0f
0a 0a 00001 010
Length
08 08
632b2b20706c7573 //"c++ plus"
15 15 00010 101
6666d6402a33 //6.7
0a
2c
41647661 6e636564 2050726f 6772616d 6d696e67 20696e20 74686520 554e4958 20456e76 69726f6e 6d656e74 //"Advanced Programming in the UNIX Environment"
15
9a998541 //16.7
30 01 //true 0011 0 000
3a 0b 78787878 20787878 207878 //"xxxx xxx xx" 0011 1 010
******************************************/
protobuf序列化规则
pb编解码关键点
Varient类型
Varient类型采用Tag + Varient编码字段值的方式,bool类型和enum类型实际上都是把二者转换为int类型进行编码,因此对于这两种类型其实效率都是比较低的。另外,Varient类型其实包含Varient和ZigZag编码格式,这两个格式共用这一种类型,只是在最后解码的时候会根据proto文件中定义不同。
Length-delimited类型
这个类型的编码结构是Tag + Length + Value
1)在消息流中每个Tag(key/键)都是varint,编码方式为:field_num << 3 | wire_type。即,Tag(key/键)由 .proto文件中字段的编号(field_num) 和 传输类型(wire_type)两部分组成。
注:Tag也是Varints编码,其后三位是传输类型(wire_type),之前的数值为是字段编号(field_num)。
注意并不是说Tag只能是一个字节,这里说了Tag也是用Varint编码,显然使用Varint编码方式几千/几万的字段序号(field_num)都是可以被表示的,
Tag的取值范围最小是1,最大是229229-1,但 19000~19999 是 protobuf 预留的,用户不能使用。
虽然 Tag 的定义范围比较大,但不同 Tag 也会对 protobuf 编码带来一些影响:
1 ~ 15:单字节编码
16 ~ 2047:双字节编码
使用频率高的变量最好设置为1 ~ 15,这样可以减少编码后的数据大小,但由于Tag一旦指定不能修改,所以为了以后扩展,也记得为未来保留一些 1 ~ 15 的 Tag
2)在对一条消息(message)进行编码的时候是把该消息中所有的key-value对序列化成二进制字节流;key和value分别采用不同的编码方式。
3)消息的二进制格式只使用消息字段的字段编号(field_num)作为Tag(key/键)的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。
4)解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个key-value对;如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。
Any 的使用
syntax = "proto3";
import "google/protobuf/any.proto";
enum Type
{
FACE = 0;
PLATE = 1;
}
message Base
{
Type type = 1;
int32 page_number = 2;
int32 result_per_age = 3;
repeated google.protobuf.Any object = 4;
}
message Face
{
string name = 1;
}
message Plate
{
string email = 1;
}
Oneof
Oneof 类似union,如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间,如
message LoginReply {
oneof test_oneof {
string name = 3;
string age = 4;
}
required string status = 1;
required string token = 2;
}
这样,name 和 age 都是 LoginReply 的成员,但不能给他们同时设置值(设置一个oneof字段会自动清理其他的oneof字段)。
升级更改 proto 需要遵循以下原则:
不要修改任何已存在的变量的 Tag
如果你新增了变量,新生成的代码依然能解析旧的数据,但新增的变量将会变成默认值。相应的,新代码序列化的数据也能被旧的代码解析,但旧代码会自动忽略新增的变量。
废弃不用的变量用 reserved 标注
int32、 uint32、 int64、 uint64 和 bool 是相互兼容的,这意味你可以更改这些变量的类型而不会影响兼容性
sint32 和 sint64 是兼容的,但跟其他类型不兼容
string 和 bytes 可以兼容,前提是他们都是UTF-8编码的数据
fixed32 和 sfixed32 是兼容的, fixed64 和 sfixed64是兼容的
=========================================================================
int类型整数编码
125 | 0x08 7d |
128 | 0x08 8001 |
129 | 0x08 8101 |
256 | 0x08 8002 |
384 | 0x08 8003 |
500 | 0x08 f403 |
512 | 0x08 8004 |
600 | 0x08 8d804 |
640 | 0x08 8005 |
10280 | 0x08 800a |
当int变量数值小于128时,采用一个字节来表示,当int类型是128的整数倍时,采用两个字节表示
如640 0x80表示 十进制128 0x05 表示640是128的5倍,0x08 Tag
500的16进制0x01f4 考虑到大小端因素,在内存中表示为0xf401,找到距离500最近的128的整数倍384 (0xf401)|| 0x8003 ==f403 【这一段是个人基于数据做的一个推断,可能真实的情况并不会这么简单】
参考文章: