ProtoBuf—3

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更小,更快
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值