1. protobuf学习

本文在学习的角度出发,进行protobuf的使用场景和功能介绍。
原文

1. protobuf介绍

  • ProtoBuf是中立于编程语言和编程平台的,可扩展的用于序列化结构化数据的解决方案,是Google公司开发的一种数据描述语言,可用于数据存储、通信协议等方面。
  • 定义完你所需要的数据结构后,你可以基于protoBuf生成各种语言的代码,这些定义的数据流可以轻松地被传递且不会破坏你已有的程序。并且你也可以更新你的结构体而现有的代码也不会收到任何影响。
  • protoBuf目前支持Objective-C、C++、C#、Jave、Python、Go、PHP、JavaScript等主流语言。

1.1 ProtoBuf使用场景说明

  工作中结构体数据的传输不仅仅局限于在函数之间传递,常规的函数间传递结构体并不需要做任特殊处理,但在实际中结构体数据的传输可能会在不同的APP之间,例如网络间传输。又或者是相同进程的不同版本,你和你的小伙伴的微信版本很可能是不同的。

例如下面这样一个结构体信息,这可能是一个微信的名片:

typedef struct User{
	char ID[20];
	int age;
	char gender;
}User;

User Boss = {"wxid_we48fhb14", 18, 1};

假设分享者分享名片时,使用如下代码发送:

sendto(sockfd, (void*)&Boss, sizeof(User),0,dest_addr,sizeof(struct sockaddr))

服务端采用如下代码接收:

	char buf[1024] = {0};
	recvfrom(sockfd, buf, 1024, 0, NULL, NULL);//不保存数据包来源地址和地址类型长度
	User* pUser = (User*)buf;
	pUser->ID;
	pUser->age;

这种简单的序列化的做法是,将传输的对象Boss转换为void*进行传输,在接收端进行反序列化就是将void*强转为结构体类型,这种方式是没有问题的。
但是这个前提是认为在发送端和接收端对于User结构体的认知是相同的,如果接收端的版本比较新,对于User的定义是这样的:

typedef struct User{
	char ID[20];
	int age;
	char gender;
	char province;
	char city;
}User;

此时在接收端进行解析数据是就会发生错误,这种情况下就需要序列化的数据传输格式。

2. 其他序列化介绍

以下介绍的方法是可用且常用的序列化方法,但是其性能并不是特别好。如果对于性能没有强烈的需求,使用起来并没有任何问题。

2.1 Json

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于其他编程语言的文本方式来存储和表示数据,同时也易于机器解析和生成,一般用于网络传输。
同时易于人阅读和编写,在一些配置脚本中也可使用json书写。
比如上面的,使用Json表示的话,可以表示为:

{
	"ID":"wxid_we48fhb14",
	"age":18,
	"gender":1,
	"province":25
}

2.1.1 使用Json序列化

C++有json的库,我们将结构体数据转化为json字符串:

#include <string>
#include <iostream>
using namespace std;
#include "JSON/JSON.h"

typedef struct User{
	char ID[20];
	int age;
	char gender;
	char province;
	char city;
}User;

User serializeToJSON(const User& user);

int main(int argc, char** argv) {
	User Boss = {"wxid_we48fhb14", 18, 1, 25};
	
	string strJSON=serializeToJSON(Boss);
	cout<<"strJSON:"<<strJSON<<endl;

	return 0;
}

//@brief:将给定的对象序列化为JSON字符串
//@param:user:对象
//@ret:JSON字符串
string serializeToJSON(const User& user) {
	JSON::FastWriter writer;
	JSON::Value person;

	person["ID"] = user.ID;
	person["age"] = user.age;
	person["gender"] = user.gender;
	person["province"] = user.province;
	
	string strJSON=writer.write(person);
	return strJSON;
}

2.1.2 Json反序列化

//@brief:将给定的JSON字符串反序列化为对象
//@param:strJSON:JSON字符串
//@ret:对象
User deserializeToObj(const string& strJSON) {
	JSON::Reader reader;
    JSON::Value value;
	User user = {0};
	
    if (reader.parse(strJSON, value)){
		strcpy(user.ID,value["ID"].asString().c_str());
		user.age=value["age"].asInt(); 
		user.gender=value["gender"].asInt();
		user.province=value["province"].asInt();
	}
	return user;
}

此时解析得到的显示province会是空,因为传输过来的数据中并没有province。

2.2 其他可选地序列化和反序列化

  • XML
  • Boost Serialization
  • MFC Framework
  • .NET Framework

3. protoBuf

protobuf相对于以上几位是效率最高且使用范围最广的。

3.1 protobuf数据类型

protobuf支持的数据类型包括:

proto文件消息类型C++类型说明
doubledouble
floatfloat
int32int32
int64int64
uint32uint32
uint64uint64
boolbool
stringstring
bytesstring

3.2 protobuf使用步骤

  1. 定义proto文件,文件的内容就是定义我们需要存储或者传输的数据结构
  2. 安装编译器,编译生成pb.hpb.cc文件
  3. 使用生成的API来读写消息。

3.2.1 定义proto文件

//user.proto
syntax = "proto3";
package tutorial;

message User{
	required string id = 1;
	required uint32 gender = 2;
	enum e_PROVINCE {
		BEIJING = 0;
		SHANGHAI = 1;
	}
	required e-PROVINCE province = 3[default = BEIJING];
}
  1. package 声明
    .proto文件以一个package声明开始。这个声明是为了防止不同项目之间的命名冲突。生成proto文件后,这个名称将会创建命名空间。
  2. 字段类型
  3. 修饰符
修饰符含义
required必须提供字段值,否则在解析时会失败
optional与required相对,可以不被赋值,在解析失将会读取到默认值,如bool类型的默认值是false
repeated字段会重复N次,这类似于是说明这是一个数组
  1. 唯一编号
    =1 =2 这些字样是必不可缺的,不可更改。
    请注意: 范围1-15中的字段编号需要一个字节进行编码,包括字段编号字段类型。范围16-2047中的字段编号需要两个字节。同时19000到19999为保留字段,不要使用。

3.2.2 编译proto文件

3.2.2.1 安装protocol buffers

下载:google下载链接
安装:按照src/README.md文件中的说明来安装

To build and install the C++ Protocol Buffer runtime and the Protocol
Buffer compiler (protoc) execute the following:
    $ ./configure
    $ make
    $ make check
    $ sudo make install
    $ sudo ldconfig # refresh shared library cache.
3.2.2.2 编译proto文件

命令:

protoc -I $SRC_DIR --cpp_out=$DST_DIR addressbook.proto

//--cpp_out :C++的类,如果是java语言则是 --java_out=

举例:

protoc user.proto --cpp_out=./

如果需要如下错误,则说明安装的动态库路径没有加载到用户搜索的动态库路径

error while loading shared libraries: libprotoc.so: cannot open shared object file: No such file or directory

可使用以下方法解决

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/$你安装的具体的路径/ProtoBuf/lib

3.2.3 使用API

//序列化和反序列化API
bool Object::SerializeToString(string* output) const; //将消息序列化并储存在指定的string中。注意里面的内容是二进制的,而不是文本;我们只是使用string作为一个很方便的容器。
bool Object::ParseFromString(const string& data); //从给定的string解析消息。
bool Object::SerializeToArray(void * data, int size) const	//将消息序列化至数组
bool Object::ParseFromArray(const void * data, int size)	//从数组解析消息
bool Object::SerializeToOstream(ostream* output) const; //将消息写入到给定的C++ ostream中。
bool Object::ParseFromIstream(istream* input); //从给定的C++ istream解析消息。

//Debug的API
string DebugString() const;	//将消息内容以可读的方式输出
string ShortDebugString() const; //功能类似于,DebugString(),输出时会有较少的空白
string Utf8DebugString() const; //Like DebugString(), but do not escape UTF-8 byte sequences.
void PrintDebugString() const;	//Convenience function useful in GDB. Prints DebugString() to stdout.

在上面的例子中使用protobuf:

#include <iostream>
#include <string>
#include "user.pb.h"
using namespace std;

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;
    tutorial::User user;
    
    // 给消息类Student对象student赋值
    user.set_gender(201421031059);
    user.set_province()="dablelv";
    user.mutable_id()="dablelv@tencent.com";
    

    // 对消息对象student序列化到string容器
    string serializedStr;
    user.SerializeToString(&serializedStr);
    cout<<std::endl<<"debugString:"<<user.DebugString();
    
/*----------------上面是序列化,下面是反序列化-----------------------*/
    tutorial::User user;
    if(!user.ParseFromString(serializedStr)){
      cerr << "Failed to parse." << endl;
      return -1;
    }

    cout <<"user debugString:"<<user.DebugString()<<endl;
    cout <<"ID: " << user.id() << endl;
    cout <<"gender: " << user.gender() << endl;

    google::protobuf::ShutdownProtobufLibrary();
}

3.3 扩展时的注意事项

随着版本的迭代,User结构体可能会增加新的字段,如果想要结构体能够兼容新老实现,那么需要遵守以下几个原则:

  1. 不要改变已存在字段的标识
  2. 不要有和删除required类型的字段
  3. 添加字段时使用新的标识

3.4 不同版本差异

差异说明
(1)在Protobuf2中,消息的字段可以加requiredoptional修饰符,也支持default修饰符指定默认值。
对于一个optional字段如果显式设置成了默认值,在序列化成二进制格式时,这个字段会被去掉,反序列化后无法使用hasXxx()方法判断,无法区分是当初没有设置还是设置成了默认值。

(2)在 Protobuf3中,去掉了requiredoptional修饰符,所有字段都是optional的,而且对于原始数据类型字段(包括repeated),不提供hasXxx()方法。

(3)缺失值和默认
缺失值或者显示设置默认值,效果是一样,序列化时字段会被去掉。
对于整数类型,默认值为0。
对于字符串,默认值为空字符串。
对于字节,默认值为空字节。
对于布尔值,默认值为false。
对于数字类型,默认值为零。
对于枚举,默认值为第一个定义的枚举值,必须为0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值