Protobuf初步使用
简介:
1)介绍了Protobuf的最基本用法;
2)Protobuf版本为3.5.0.1;
相关内容:
1)Protobuf+Cpp的基本用法;
2)使用protobuf时的消息边界问题;
一、Protobuf+Cpp的基本用法
Protobuf可以解析.proto,然后生成消息协议需要的代码,包括.pb.h和.pb.cc。在使用时,搭配上libprotobuf.lib即可。
而对于proto文件的解析有两种,即静态和动态。先说动态。顾名思义,在程序运行的过程中解析proto文件,生成消息结构。换言之,这种方式把.proto文件作为程序的配置文件使用。若有兴趣,可以参考这位的博客。
这里主要介绍静态用法。这与需求有关。因为是初次使用,所以更关心它生成的消息结构长什么样,而且暂时没有任意配置消息结构的需求。这位的博客对于这种方式做了很好的介绍。不过他所用的protobuf版本并非Protobuf3。
这里介绍最简洁的用法。
1.1 首先是.proto文件,如下:
syntax = "proto3"; # 指明版本
package my_package; # 生成cpp的命名空间
message Person { # 生成cpp的类
string name = 1;
int32 id = 2;
}
message AddressBook {
repeated Person person = 2;
}
1.2 使用protoc工具生成代码,使用方法如下。然后在output目录下会生成addressbook.pb.h和addressbook.pb.cc。若不加-I选项,在output目录下会新建文件夹,以放置代码。
protoc -I=./demo --cpp_out=./output ./demo/addressbook.proto
1.3 将addressbook.pb.h和addressbook.pb.cc,以及libprotobuf.lib放置到工程目录,即可使用。
1.4 生成的类有AddressBook和Person。
在proto文件中,AddressBook包含的是repeated Person person
。所以在使用时,可以是这样:
using namespace my_package;
AddressBook ab;
Person * person = ab.add_person();
然后,对于person的成员的操作,可以是这样:
person->set_name("abc"); // 设置
person->name(); // 获取
1.5 假设只是把protobuf作为数据操作的协议,那到这里就可以使用了。然而,protobuf更多的是作为传输协议,所以还有序列化和反序列化的问题。对于类AddressBook的序列化/反序列化,可以像这样:类和string之间的转换:
string str = ab.SerializeAsString(); // 从类->string
AddressBook cd;
cd.ParseFromString(str); // 从string->类
或者可以这样:类和文件流之间的转换:
ofstream output("./test", ios::trunc|ios::binary);
ab.SerializeToOstream(&output); // 从类->文件流
output.close();
ifstream input("./test", ios::binary);
cd.ParseFromIstream(&input); // 从文件流->类
input.close();
1.6 最后也是最重要的,关于版本校验和内存的释放
主要是protobuf的一个宏定义和一个函数。这里直接展示它们的代码注释。
GOOGLE_PROTOBUF_VERIFY_VERSION:
// 将这个宏放置在main函数中(或任何你想要使用protobuf库的地方之前),
// 以此确认你用protobuf生成的头文件和库的版本是匹配的。
// 如果检测到版本不一致,程序将被中断。
#define GOOGLE_PROTOBUF_VERIFY_VERSION \
::google::protobuf::internal::VerifyVersion( \
GOOGLE_PROTOBUF_VERSION, GOOGLE_PROTOBUF_MIN_LIBRARY_VERSION, \
__FILE__)
google::protobuf::ShutdownProtobufLibrary();
// 关闭protobuf库,释放由库或者*.pb.cc分配的内存。
// 两种情况下,你会需要调用这个函数:
// 1. 有严格的内存管理要求,即每个malloc都要对应一个free,即使是对于那些要一直存活到进程终止的对象;
// 2. 所编写的程序是一个动态加载库,且在退出时需要自行清理资源;
// 这个函数可以重复多次调用。但是,在调用了这个函数之后,再去使用protobuf库的其它部分,是很不安全的。
LIBPROTOBUF_EXPORT void ShutdownProtobufLibrary();
二、使用protobuf时的消息边界问题
protobuf似乎是将消息边界的处理留给了我们自己。这在UDP中使用起来倒是问题不大,而在TCP时则需要考虑边界的问题,比如说一次收到了两个AddressBook对象的数据。
可以尝试一个例子:先将一个类序列化为string,然后字符串x2之后,再反序列化为类,像这样:
AddressBook ab;
ab.add_person();
cout << ab.person().size() << endl; // 输出1
string str = ab.SerializeAsString();
str += str;
AddressBook cd;
cd.ParseFromString(str);
cout << cd.person().size() << endl; // 输出2
所以,在使用时,可以在数据前后加上数据头和尾,比如像这样:
struct Head; // 可以包含数据长度,版本等信息
struct Tail; // 可以包含校验和,终止标识等信息
struct Msg
{
Head head;
string data; // protobuf的对象序列化后的结果
Tail tail;
string Serialize()
{
string buf = string((char*)&head, sizeof(head));
buf += data;
buf += string((char*)&tail, sizeof(tail));
return buf;
}
void Deserialize(string buf)
{
/*还需要最小长度校验之类,这里先略去*/
head = *(Head*)buf.c_str(); // 取消息头
int size = head.size; // 消息体长度
data = buf.substr(sizeof(Head), size); // 取消息体
tail = *(Tail*)(buf.c_str()+sizeof(Head)+size);
}
};