Protobuf 编码规则及c++使用详解
Protobuf 介绍
Protocol Buffers (a.k.a., protobuf) are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data
Protocol Buffers(简称为protobuf)是谷歌的语言无关、平台无关、可扩展的机制,用于序列化结构化数据。
protobuf的主要目的是在不同应用程序之间高效地传输和存储数据。与XML和JSON等文本格式相比,protobuf的编码格式更紧凑,解析速度更快,占用的存储空间更小。此外,protobuf还支持版本兼容性,使得在数据结构发生变化时能够向后兼容。
在音视频领域,信令和打点数据量非常大,采用JSON等文本方式,虽然可读性较大,但数据提交太大,不利于传输和节省带宽,因此二进制编码方式(protobuf)非常适合这个领域。
c++ 使用详解
1、定义.proto文件 (message.proto)
syntax = "proto3";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
包含了常用的整型,字符串,枚举,结构体,repeated(数组)类型
2、使用protoc编译器生成c++源码文件(message.pb.h, message.pb.cc)
protoc --cpp_out=. message.proto
libprotobuf和protoc编译器如何安装?
在ubuntu系统下:
# sudo apt update
# sudo apt install libprotobuf-dev protobuf-compiler
3、编写测试文件(main.cpp)
// g++ -std=c++11 main.cpp message.pb.cc -lprotobuf -o main
#include "message.pb.h"
#include <google/protobuf/util/json_util.h>
#include <iostream>
#include <fstream>
#include <initializer_list>
struct CppPerson {
std::string name = "John";
int32_t id = 1;
std::string email = "john@163.com";
enum CppPhoneType {
UNSPECIFIED = 0,
MOBILE = 1,
HOME = 2,
WORK = 3,
};
struct CppPhoneNumber {
std::string number;
CppPhoneType type;
};
std::vector<CppPhoneNumber> phones;
};
int main() {
// Create a CppPerson object
CppPerson cpp_person;
cpp_person.phones.push_back({"123456789", CppPerson::HOME});
cpp_person.phones.push_back({"987654321", CppPerson::WORK});
// 创建一个 Person 对象
tutorial::Person person;
person.set_name(cpp_person.name);
person.set_id(cpp_person.id);
person.set_email(cpp_person.email);
for (auto &s : cpp_person.phones) {
auto phone = person.add_phones();
phone->set_number(s.number);
phone->set_type(static_cast<tutorial::Person::PhoneType>(s.type));
}
// 将 Person 对象序列化为字节流
std::string serialized;
person.SerializeToString(&serialized);
std::cout << "serialized size: " << serialized.size() << std::endl;
// message <--> json
std::string json;
google::protobuf::util::JsonOptions json_options;
// json_options.add_whitespace = true;
google::protobuf::util::MessageToJsonString(person, &json, json_options);
std::cout << "JSON: " << json << std::endl;
std::cout << "JSON size: " << json.size() << std::endl;
tutorial::Person person_from_json;
google::protobuf::util::JsonStringToMessage(json, &person_from_json);
std::string person_from_json_serialized;
person_from_json.SerializeToString(&person_from_json_serialized);
std::cout << "Person from JSON serialized size: " << person_from_json_serialized.size() << std::endl;
// 将字节流写入文件
std::fstream output("person.bin", std::ios::out | std::ios::binary);
output.write(serialized.c_str(), serialized.size());
output.close();
// 从文件中读取字节流
std::fstream input("person.bin", std::ios::in | std::ios::binary);
std::string serialized_input((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
// 将字节流反序列化为 Person 对象
tutorial::Person person2;
person2.ParseFromString(serialized_input);
// 输出 Person 对象的信息
std::cout << "Person:" << std::endl;
std::cout << " name: " << person2.name() << std::endl;
std::cout << " id: " << person2.id() << std::endl;
std::cout << " email: " << person2.email() << std::endl;
for (int i = 0; i < person2.phones_size(); i++) {
auto &phone_number = person2.phones(i);
std::cout << " phone number: " << phone_number.number() << std::endl;
std::cout << " phone type: " << phone_number.type() << std::endl;
}
return 0;
}
4、编译运行
# g++ -std=c++11 main.cpp message.pb.cc -lprotobuf -o main
# ./main
serialized size: 52
JSON: {"name":"John","id":1,"email":"john@163.com","phones":[{"number":"123456789","type":"PHONE_TYPE_HOME"},{"number":"987654321","type":"PHONE_TYPE_WORK"}]}
JSON size: 152
Person from JSON serialized size: 52
Person:
name: John
id: 1
email: john@163.com
phone number: 123456789
phone type: 2
phone number: 987654321
phone type: 3
查看编码好的二进制(person.bin)
5、编码后二进制流分析
编码规则
第一个字节包含了字段编号(Filed Number)和wired type, 组合起来代表message里一个字段的Key信息
bit 7
0-代表该字节自解释完整
1-代表后续还有字节,需要整合后续字节提取信息 (当字段数超过15个是采用这种方式,即由多个字节编码key)
<这个bit就是varint的关键功能位,variant中每个字节的最高位bit称之为most significant bit(MSB),如果该bit为0意味着这个字节为表示当前整数的最后一个字节,如果为1则表示后面还有至少1个字节,可见,varint的终止位置其实是自解释的。>
bit 6-bit 3
存储字段编号,field number (fn)
bit 2-bit 0
存储wired type (wt)
常用的类型主要有VARINT和LEN
- wire type=0、1、5,编码为key+数据,只有一个数据,可能占数个字节,数据在编码时自带终止标记;
- wire type=2,编码为key+length+数据,length指示了数据长度,可能有多个数据,顺序排序
总结:二进制流存储方式为key data key data key data…
比如上述例子:
6、番外:protobuf和JSON互转
如上述示例,转换为的json为:
JSON: {"name":"John","id":1,"email":"john@163.com","phones":[{"number":"123456789","type":"PHONE_TYPE_HOME"},{"number":"987654321","type":"PHONE_TYPE_WORK"}]}
JSON size: 152
可见JSON紧压缩方式大小为152Byte,而PB压缩大小为52Byte,非常省空间。
Protobuf,它只需要简单地将一个二进制序列,按照指定的格式读取到C++对应的结构类型中就可以了。消息的decoding过程也可以通过几个位移操作组成的表达式计算即可完成。而对于字符串、自定义对象类型的数据,Protobuf在存储的时候,也存储了该数据的字节长度,读取起来也非常快。
参考
https://protobuf.dev/overview/