文章目录
- 1、默认值
- 2、更新规则
- 3、未知字段
- 4、option字段
- 5、通信录网络版
- 6、总结
1、默认值
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
对于字符串,默认值为空字符串。
对于字节(bytes),默认值为空字节。
对于布尔值,默认值为false。
对于数值类型,默认值为0。
对于枚举,默认值是第一个定义的枚举值,必须为0。
对于消息字段,未设置该字段。它的取值是依赖于语言。
对于设置了repeated的字段的默认值是空的(通常是相应语言的一个空列表)。
对于消息字段、oneof字段和any字段,C++和Java语言中都有has_方法来检测当前字段是否被设置。
对于标量数据类型,在proto3语法下,没有生成has_方法。
2、更新规则
如果现有的消息类型已经不再满足我们的需求,例如需要扩展一个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。遵循如下规则即可:
新增:
不要和老字段冲突即可
修改:
1、禁止修改任何已有字段的字段编号。
2、int32,uint32,int64,uint64和 bool是完全兼容的。可以从这些类型中的一个改为另一个,而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与C++一致的处理方案(例如,若将64位整数当做32位进行读取,它将被截断为32位)。
3、sint32和sint64相互兼容但不与其他的整型兼容。
4、string 和 bytes在合法UTF-8字节前提下也是兼容的。
5、bytes包含消息编码版本的情况下,嵌套消息与bytes也是兼容的。
6、fixed32 与sfixed32兼容,fixed64与sfixed64兼容。
7、enum 与int32,uint32,int64和uint64兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的proto3枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
8、oneof:
将一个单独的值更改为新oneof类型成员之一是安全和二进制兼容的。
若确定没有代码一次性设置多个值那么将多个字段移入一个新oneof类型也是可行的。
将任何字段移入已存在的 oneof类型是不安全的。
删除:
不能直接删除已经存在的老字段,否则在实现业务代码逻辑上,会造成一些影响,比如数据损坏等
如果要删除老字段,要保证不使用已经被删除的或者已经被注释掉的字段编号
举个例子:
最开始client和service共用同一份ProtoBuf文件,只是包名不同,但由于某些情况需要int32 age = 2;改为int32 birthday = 2;
#service
syntax = "proto3";
package s_contacts;
//联系人
message PeopleInfo
{
string name = 1; //姓名
//int32 age = 2; //年龄
int32 birthday = 2; //生日
message Phone
{
string number = 1;
}
repeated Phone phone = 3; //电话号码
}
//通讯录
message Contacts
{
repeated PeopleInfo contacts = 1;
}
//service.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(s_contacts::PeopleInfo *people)
{
cout << "新增联系人" << endl;
cout << "请输入联系人姓名:";
string name;
getline(cin, name);
people->set_name(name);
cout << "请输入联系人生日:";
int birthday;
cin >> birthday;
people->set_birthday(birthday);
cin.ignore(256, '\n');
for (int i = 0;; ++i)
{
cout << "请输入联系人电话" << i + 1 << "(只输入回车完成电话新增):";
string number;
getline(cin, number);
if (number.empty())
{
break;
}
s_contacts::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
}
cout << "添加联系人成功" << endl;
}
int main()
{
s_contacts::Contacts contacts;
// 读取本地已经存在的通讯录文件
fstream input("../contacts.bin", ios::in | ios::binary);
if (!input)
{
cout << "contacts.bin not find, create new file!" << endl;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "parse error" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件
fstream output("../contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output))
{
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
input.close();
output.close();
return 0;
}
#client
syntax = "proto3";
package c_contacts;
//联系人
message PeopleInfo
{
string name = 1; //姓名
int32 age = 2; //年龄
message Phone
{
string number = 1;
}
repeated Phone phone = 3; //电话号码
}
//通讯录
message Contacts
{
repeated PeopleInfo contacts = 1;
}
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(c_contacts::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); ++i)
{
cout << "------------联系人" << i + 1 << "------------" << endl;
const ::c_contacts::PeopleInfo &people = contacts.contacts(i);
cout << "姓名" << people.name() << endl;
cout << "年龄" << people.age() << endl;
for (int j = 0; j < people.phone_size(); ++j)
{
const ::c_contacts::PeopleInfo_Phone &phone = people.phone(j);
cout << "电话" << j + 1 << ":" << phone.number() << endl;
}
}
}
int main()
{
c_contacts::Contacts contacts;
// 读取本地已存在的文件
fstream input("../contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input))
{
cerr << "parse error" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
input.close();
return 0;
}
数据经过序列化,反序列化后数据就会不一致
如果非要删除,再重新添加,就需要用到reserved字段,被reserved修饰的编号成为保留编号,reserved不仅能修饰编号,还能修饰名称
syntax = "proto3";
package s_contacts;
//联系人
message PeopleInfo
{
reserved 2, 10 to 12;
reserved "age";
string name = 1; //姓名
//int32 age = 2; //年龄
int32 birthday = 2; //生日
message Phone
{
string number = 1;
}
repeated Phone phone = 3; //电话号码
}
//通讯录
message Contacts
{
repeated PeopleInfo contacts = 1;
}
如果此时还是用了保留编号或者名称,则会编译报错
将birthday编号改为4,再编译,然后再测试,统一了
3、未知字段
在前面一小节中,我们向service中的contacts.proto新增了‘生日’字段,但对于client相关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这里要说的是,新增的‘生日’字段在旧程序(client)中其实并没有丢失,而是会作为旧程序的未知字段。
本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引入了对未知字段的保留机制。所以在3.5或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
修改client.cc中的代码,获取对应的未知字段
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace c_contacts;
using namespace google::protobuf;
void PrintContacts(c_contacts::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); ++i)
{
cout << "------------联系人" << i + 1 << "------------" << endl;
const ::c_contacts::PeopleInfo &people = contacts.contacts(i);
cout << "姓名" << people.name() << endl;
cout << "年龄" << people.age() << endl;
for (int j = 0; j < people.phone_size(); ++j)
{
const ::c_contacts::PeopleInfo_Phone &phone = people.phone(j);
cout << "电话" << j + 1 << ":" << phone.number() << endl;
}
// 通过GetReflection()拿到对应的Reflection*
const Reflection *reflection = PeopleInfo::GetReflection();
const UnknownFieldSet &set = reflection->GetUnknownFields(people);
for (int j = 0; j < set.field_count(); ++j)
{
const UnknownField &unknown_field = set.field(j);
cout << "未知字段:" << j + 1 << " "
<< "编号:" << unknown_field.number();
switch (unknown_field.type())
{
case UnknownField::Type::TYPE_VARINT:
cout << " 值:" << unknown_field.varint() << endl;
break;
case UnknownField::Type::TYPE_LENGTH_DELIMITED:
cout << " 值:" << unknown_field.length_delimited() << endl;
break;
//case...
default:
break;
}
}
}
}
int main()
{
c_contacts::Contacts contacts;
// 读取本地已存在的文件
fstream input("../contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input))
{
cerr << "parse error" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
input.close();
return 0;
}
结果:
4、option字段
ProtoBuf为我们提供了option字段,它用于为消息、字段或枚举定义附加的元数据和配置选项。Option 字段允许您在不修改消息定义的情况下,为特定的消息或字段提供额外的指令和配置。
举一个简单了例子:
syntax = "proto3";
message PeopleInfo
{
string name = 1;
}
对上述proto文件进行编译,查看对应的.h文件
对proto文件进行修改,再编译,再查看.h文件
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
message PeopleInfo
{
string name = 1;
}
对比发现,两者的代码确实不同
选项分类
选项的完整列表在google/protobuf/descriptor.proto中定义。部分代码如下:
syntax = "proto2"; //descriptor.proto 使用proto2语法版本
message Fileoptions { ... } //文件选项定义在FileOptions消息中
message Messageoptions { ... } //消息类型选项定义在MessageOptions消息中
message Fieldoptions { ... } //消息字段选项定义在FieldOptions消息中
message Oneof0ptions { ... } //oneof字段选项定义在 OneofOptions消息中
message EnumOptions { ... } //枚举类型选项定义在EnumOptions消息中
message EnumValueOptions { ... } //枚举值选项定义在 EnumValueOptions消息中
message Serviceoptions { ... } //服务选项定义在Serviceoptions消息中
message MethodOptions { ... } //服务方法选项定义在Method0ptions消息中
......
由此可见,选项分为文件级、消息级、字段级等等,但并没有一种选项能作用于所有类型
常用选项列举
optimize_for:该选项为文件选项,可以设置protoc编译器的优化级别,分别为SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响,设置不同的优化级别,编译.proto文件后生成的代码内容不同。
- SPEED : protoc编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间。SPEED 是默认选项。
- CODE_SIZE : proto编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和SPEED恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto文件,但并不盲目追求速度的应用中。
- LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供encoding+序列化功能,所以我们在链接BP库时仅需链接libprotobuf-lite,而非libprotobuf。这种模式通常用于资源有限的平台,例如移动手机平台中。
allow_alias:允许将相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。
举个例子:
enum PhoneType
{
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; //若不加 option allow_alias = true; 这一行则会编译报错
}
ProtoBuf允许自定义选项并使用。但该功能大部分场景都用不到,有兴趣的可以参考
https://developers.google.cn/protocol-buffers/docs/proto?hl=zh-cn#customoptions
5、通信录网络版
对前面所学到的知识进行总结,设计一个网络版通信录,只实现了新增功能。
在此之前需要下载一下cpp-httplib开源库:
https://gitcode.net/mirrors/yhirose/cpp-httplib?utm_source=csdn_githup_accelerator
整体的目录结构
service端:
main.cc
#include <iostream>
#include "httplib.h"
#include "add_contact.pb.h"
using namespace std;
using namespace httplib;
class ContactsExecption
{
private:
std::string message;
public:
ContactsExecption(std::string str = "A problem") : message(str){}
std::string what() const
{
return message;
}
};
void PrintContacts(add_contact::AddContactRequest &req)
{
cout << "姓名" << req.name() << endl;
cout << "年龄" << req.age() << endl;
for (int j = 0; j < req.phone_size(); ++j)
{
const ::add_contact::AddContactRequest_Phone &phone = req.phone(j);
cout << "电话" << j + 1 << ":" << phone.number();
// 联系人电话1:123456789 (MP)
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
}
}
static unsigned int random_char()
{
//用于随机数引擎获得随机种子
std::random_device rd;
//mt19973是C++11新特性,它是一种随机数算法,用法与rand()函数类似,但是mt19973具有速度快,周期长的特点
//作用是生成伪随机
std::mt19937 gen(rd());
//随机生成一个整数i,范围[0,255]
std::uniform_int_distribution<> dis(0, 255);
return dis(gen);
}
// 生成UUID(通用唯一标识符)
static std::string generate_hex(const unsigned int len)
{
std::stringstream ss;
//生成 len 个16进制随机数,将其拼接而成
for(auto i = 0; i < len; ++i)
{
const auto rc = random_char();
std::stringstream hexstream;
//十六进制输出
hexstream << std::hex << rc;
auto hex = hexstream.str();
ss << (hex.length() < 2 ? '0' + hex : hex);
}
return ss.str();
}
void set_uid()
{
}
int main()
{
cout << "————————————————启动服务————————————————" << endl;
Server server;
server.Post("/contacts/add", [](const Request &req, Response &res) {
cout<<"收到post请求"<<endl;
//反序列化 request:req.bady
add_contact::AddContactRequest request;
add_contact::AddContactResponse response;
try
{
if(!request.ParseFromString(req.body))
{
throw ContactsExecption("AddContactRequest反序列化失败!");
}
//新增联系人,持久化存储通讯录----->打印新增联系人信息
PrintContacts(request);
//构造 response:res.body
response.set_success(true);
response.set_uid(generate_hex(10));
//res.body(序列化response)
string response_str;
if(!response.SerializeToString(&response_str))
{
throw ContactsExecption("AddContactResponse序列化失败!");
}
res.status = 200;
res.body = response_str;
res.set_header("Content-Type", "application/protobuf");
}catch(const ContactsExecption& e) {
res.status = 500;
response.set_success(false);
response.set_error_desc(e.what());
string response_str;
if(response.SerializeToString(&response_str))
{
res.body = response_str;
res.set_header("Content-Type", "application/protobuf");
}
cout << "/contacts/add 发生异常,异常信息" << e.what() << endl;
}
});
// 绑定8080端口,并且将端口对外开放
server.listen("0.0.0.0", 8080);
return 0;
}
add_contact.proto:
syntax = "proto3";
package add_contact;
message AddContactRequest
{
string name = 1; //姓名
int32 age = 2; //年龄
message Phone
{
string number = 1;
enum PhoneType
{
MP = 0; //移动电话
TEL = 1;//固定电话
}
PhoneType type = 2;
}
repeated Phone phone = 3;
}
message AddContactResponse
{
bool success = 1; //服务是否调用成功
string error_desc = 2; //错误原因
string uid = 3;
}
makefile:
service:main.cc add_contact.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf -lpthread
.PHONY:clean
clean:
rm -f service
client端:
main.cc:
#include <iostream>
#include "httplib.h"
#include "ContactsExecption.h"
#include "add_contact.pb.h"
using namespace std;
using namespace httplib;
#define CONTACTS_HOST "127.0.0.1"
#define CONTACTS_PORT 8080
void menu()
{
cout << "---------------------------------------------------" << endl
<< "----------------请选择对通信录的操作-----------------" << endl
<< "-------------------1、新增联系人--------------------" << endl
<< "-------------------2、删除联系人--------------------" << endl
<< "-------------------3、查看联系人列表----------------" << endl
<< "-------------------4、查看联系人的详细信息-----------" << endl
<< "-------------------0、退出--------------------------" << endl
<< "---------------------------------------------------" << endl;
}
void buildAddContactRequest(add_contact::AddContactRequest* req)
{
cout << "新增联系人" << endl;
cout << "请输入联系人姓名:";
string name;
getline(cin, name);
req->set_name(name);
cout << "请输入联系人年龄:";
int age;
cin >> age;
req->set_age(age);
cin.ignore(256, '\n');
for (int i = 0;; ++i)
{
cout << "请输入联系人电话" << i + 1 << "(只输入回车完成电话新增):";
string number;
getline(cin, number);
if (number.empty())
{
break;
}
add_contact::AddContactRequest_Phone* phone = req->add_phone();
phone->set_number(number);
cout << "请输入该电话类型(1.移动电话 2.固定电话): ";
int type;
cin >> type;
cin.ignore(256, '\n');
switch (type)
{
case 1:
phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);
break;
default:
cout << "选择有误" << endl;
break;
}
}
}
void addContact()
{
Client cli(CONTACTS_HOST, CONTACTS_PORT);
//构造 req
add_contact::AddContactRequest req;
buildAddContactRequest(&req);
//序列化
string req_str;
if(!req.SerializeToString(&req_str))
{
throw ContactsExecption("AddContactRequest序列化失败!");
}
//发起post调用
auto res = cli.Post("/contacts/add", req_str, "application/protobuf");
if(!res)
{
string err_desc;
err_desc.append("/contacts/add 链接失败!错误信息:" )
.append(httplib::to_string(res.error()));
throw ContactsExecption(err_desc);
}
//反序列化 resp
add_contact::AddContactResponse resp;
bool parse = resp.ParseFromString(res->body);
//反序列化失败并且status不为200
if(!parse && res->status != 200)
{
string err_desc;
err_desc.append("/contacts/add 调用失败!" )
.append(std::to_string(res->status))
.append("(").append(res->reason).append(")");
throw ContactsExecption(err_desc);
}
//反序列化成功但status不为200
else if(res->status != 200)
{
string err_desc;
err_desc.append("/contacts/add 调用失败!" )
.append(std::to_string(res->status))
.append("(").append(res->reason).append(") 错误原因:")
.append(resp.error_desc());
throw ContactsExecption(err_desc);
}
//判断服务端是否序列化成功
else if(!resp.success())
{
string err_desc;
err_desc.append("/contacts/add 结果异常" )
.append("异常原因:")
.append(resp.error_desc());
throw ContactsExecption(err_desc);
}
//打印结果
cout << "新增联系人成功,联系人ID:" << resp.uid() << endl;
}
int main()
{
enum POTION
{
QUIT = 0,
ADD,
DEL,
FIND_ALL,
FIND_ONE
};
while (true)
{
menu();
cout << "请选择:";
int choose;
cin >> choose;
getchar();
try
{
switch (choose)
{
case POTION::QUIT:
cout << "程序退出" << endl;
return 0;
case POTION::ADD:
addContact();
case POTION::DEL:
case POTION::FIND_ALL:
case POTION::FIND_ONE:
break;
default:
cout << "选择有误,请重新选择" << endl;
}
} catch(const ContactsExecption& e)
{
cout << "操作通讯录时发生异常" <<endl
<< "异常信息:" << e.what() <<endl;
}
}
return 0;
}
add_contact.proto:
syntax = "proto3";
package add_contact;
message AddContactRequest
{
string name = 1; //姓名
int32 age = 2; //年龄
message Phone
{
string number = 1;
enum PhoneType
{
MP = 0; //移动电话
TEL = 1;//固定电话
}
PhoneType type = 2;
}
repeated Phone phone = 3;
}
message AddContactResponse
{
bool success = 1; //服务是否调用成功
string error_desc = 2; //错误原因
string uid = 3;
}
makefile:
client:main.cc add_contact.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf -lpthread
.PHONY:clean
clean:
rm -f client
最终的运行结果:
6、总结
序列化协议 | 通用性 | 格式 | 可读性 | 序列化大小 | 序列化性能 | 适用场景 |
---|---|---|---|---|---|---|
JSON | 通用(json,xml已经成为多种行业标准的编写工具) | 文本格式 | 好 | 轻量(使用键值对方式,压缩了一定的空间) | 中 | web项目。因为浏览器对json数据支持非常好,有很多内建的函数支持 |
XML | 通用 | 文本格式 | 好 | 重量(数据冗余,因为需要成对的闭合标签) | 低 | XML作为一种扩展标记语言,衍生出了HTML、RDF/RDFS,它强调数据结构化的能力和可读性 |
ProtoBuf | 独立(ProtoBuf只是Google内部的工具) | 二进制格式 | 差(只能反序列化后得到真正可读的数据) | 轻量(比json更轻量,传输起来带宽和速度会有优化) | 高 | 适合高性能,对响应速度有要求的数据传输场景。ProtoBuf比JSON、XML更小,更快 |