1 序列化
序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程,与之相对应的过程称之为反序列化(Unserialization)。序列化和反序列化主要用于解决在跨平台和跨语言的情况下, 模块之间的交互和调用,但其本质是为了解决数据传输问题。
1.1 如何实现序列化
- 要有原始数据
- 复合数据类型——常见
- 基础数据类型
- 获得的序列化后数据有什么作用?
- 目的:进行分发,分发到不同的平台或终端,保证不同的平台能正常解析
- 例如:网络传输、磁盘拷贝
- 序列化是为了解决传输问题,不是为了加密
- 序列化整体过程:
- 发送端
- 原始数据 -> 序列化(编码)-> 特殊格式的字符串
- 发送这个字符串
- 接收端
- 接收数据
- 特殊格式的字符串 -> 反序列化(解码)-> 原始数据
- 对原始数据进行处理
- 发送端
1.2 为什么需要序列化
在网络传输过程中会发送什么问题?
- 平台不同的跨平台传输,例如32bit/64bit的计算机对应的模型数据结构占的内存不同
- 如果传输的不是字符串,就会有字节序的问题
- 我们知道字符在内存中占一个字节,所以字符没有字符序问题
- 如果发的数据时结构体类型,直接调用linux的send函数就会在传输中出现问题
- 采用的编程语言不同也会导致相同数据类型所占的内存不同
- C -> char -> 占一个字节
- java -> char -> 占两个字节
- 字节对齐问题
1.3 常用的序列化方式
- XML(Extensible Markup Language )
- Json( JavaScript Object Notation )
- Protocol Buffer
- ASN.1 抽象语法标记(Abstract Syntax Notation One)
- boost 序列化的类
本文主要讨论的时Json和protobuf的使用方法
2 JSON
JSON起源于弱类型语言Javascript,它的产生来自于一种称之为"关联数组(Associative array)"的概念,其本质是就是采用"键值对"的方式来描述对象。JSON格式保持了XML的人眼可读的优点,非常符合工程师对对象的理解。相对于XML而言,序列化后的数据更加简洁(XML所产生序列化之后文件的大小接近JSON的两倍),而且其协议比较简单,解析速度比较快。JSON格式具备Javascript的先天性支持,所以被广泛应用于Web browser的应用常景中,是Ajax的事实标准协议。
2.1 Json组合数据格式
- json数组
- 类似于C++数组
- 区别:但是json数据内的数据类型可以不同
- 可包含的数据类型: [int, double, float, bool, string, char*, json数组, json对象]
- 举例:[12, 13.45, "hello, world", true, false, [1,2,"aa"], {"a":"b"}]
- json对象
- 使用{}表示
- 分为两部分:key,value
- 键值对间使用逗号间隔
- key:必须时字符串
- value:要保存的数据,可以有多种类型
- 整形, 浮点, 字符串, 布尔, json数组, json对象
2.2 jsoncpp类使用
1)在内存中组织json格式的数据写入磁盘
- 一般在组织数据前我们都必须确定先确定根节点——即使用json数据还是使用json对象
- jsoncpp类中给我们提供了以下的方法供我们调用
// Json支持的数据类型
Type = {int, double, float, string, char*, bool, JsonArray, JsonObject}
// 构造函数
Value(ValueType type = nullValue);
Value(Int value);
Value(UInt value);
#if defined(JSON_HAS_INT64)
Value(Int64 value);
Value(UInt64 value);
#endif // if defined(JSON_HAS_INT64)
Value(double value);
Value(const char* value); ///< Copy til first 0. (NULL causes to seg-fault.)
Value(const char* begin, const char* end);
// 将Value对象转换成对应类型的数据
Int asInt() const;
UInt asUInt() const;
#if defined(JSON_HAS_INT64)
Int64 asInt64() const;
UInt64 asUInt64() const;
#endif // if defined(JSON_HAS_INT64)
LargestInt asLargestInt() const;
LargestUInt asLargestUInt() const;
float asFloat() const;
double asDouble() const;
bool asBool() const;
// 判断Value对象中存储的数据的类型
bool isNull() const;
bool isBool() const;
bool isInt() const;
bool isInt64() const;
bool isUInt() const;
bool isUInt64() const;
bool isIntegral() const;
bool isDouble() const;
bool isNumeric() const;
bool isString() const;
bool isArray() const;
bool isObject() const;
// 取值
// 格式化 -> 将对象转换为字符串
// 适合于查看信息或者写文件
std::string toStyledString() const;
使用举例:
#include <iostream>
#include <fstream>
#include <json/json.h>
int main() {
// 创建一个 JSON 对象
Json::Value root;
// 填充 JSON 数据
root["name"] = "Alice";
root["age"] = 30;
root["is_student"] = false;
// 创建一个 JSON 数组
Json::Value courses(Json::arrayValue);
courses.append("Mathematics");
courses.append("Physics");
courses.append("Computer Science");
root["courses"] = courses;
// 创建一个嵌套的 JSON 对象
Json::Value address;
address["street"] = "123 Main St";
address["city"] = "Anytown";
address["zip"] = "12345";
root["address"] = address;
// 使用 toStyledString() 方法将 JSON 对象转换为字符串
std::string jsonString = root.toStyledString();
// 将 JSON 数据写入文件
std::ofstream file("data.json");
if (!file.is_open()) {
std::cerr << "Error opening file for writing" << std::endl;
return 1;
}
// 写入 JSON 字符串到文件
file << jsonString;
file.close();
std::cout << "JSON data has been written to data.json" << std::endl;
return 0;
}
- 我们可以尝试阅读以上代码,是否能在脑海中构造出json格式的数据
- 上面例子所示我们使用的时json对象为根节点。即所有的数据都是被一个大括号{}包含
- json的根对象中有json数据(key和value组成的简单数据)
- 有json数组courses
- 有嵌套的json对象address
{
"address" : {
"city" : "Anytown",
"street" : "123 Main St",
"zip" : "12345"
},
"age" : 30,
"courses" : [
"Mathematics",
"Physics",
"Computer Science"
],
"is_student" : false,
"name" : "Alice"
}
2)读取磁盘中的json格式的文件写入内存
当项目或者某些业务场景中的某些数据要使得更加灵活时,或者是这些数据经常会改变时。我们常常会将这种数据写入json的配置文件中,例如TCP传输时候的端口、地址等信息,我们只需要读取其中json文件的内容进行初始化,有需要更改时更改json文件的数据即可
- jsoncpp中给我们提供了reader类,我们可以使用该类进行解析数据
// json格式字符串 -> Value对象
// c++
bool parse(const std::string& document, Value& root, bool collectComments = true);
参数:
- document: json字符串, 传入参数
- root: 传出参数, 转换完成之后的Value对象
// c用法
bool parse(const char* beginDoc, const char* endDoc,
Value& root, bool collectComments = true);
参数:
- beginDoc: 字符串起始地址
- endDoc: 字符串结束地址
- root: 传出参数, 转换完成之后的Value对象
// c++用法
bool parse(std::istream& is, Value& root, bool collectComments = true);
参数:
- is: 文件流对象, 使用这个流对象打开一个磁盘文件
- root: 传出参数, 转换完成之后的Value对象
举例:
Server::Server(string json)
{
//参数json为要打开的文件名字
ifstream ifs(json);
Reader r;
Value val;
r.parse(ifs, val);
m_port = val["port"].asInt();
}
- reader类使用起来其实非常简单,比如在上面的构造函数中,我们可以通过读取json文件对某些变量进行初始化(比如上面初始化的是通信的端口) 。只需要指定一个文件流和一个value值即可。文件流用于读文件,value对象用于传出。我们就可以调用reader类的parse函数将其解析出来
3 protobuf
Protocol Buffer( 简称 Protobuf) 是Google公司内部的混合语言数据标准,它是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或RPC数据交换格式。Protobuf是一个纯粹的展示层协议,可以和各种传输层协议一起使用,Protobuf的文档也非常完善。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。Protobuf支持的数据类型相对较少,不支持常量类型。由于其设计的理念是纯粹的展现层协议,目前并没有一个专门支持Protobuf的RPC框架。
更多资料可查看:https://developers.google.com/protocol-buffers/
3.1 protobuf数据组织格式
假设我们要序列化的数据为一个结构体(当然protobuf数据一般都是客户端和服务端相互协商):
// 要序列化的数据
struct Person
{
int id;
string name;
string sex; // man woman
int age;
};
// protobuf的版本
syntax = "proto3"; // proto2
// 组织Persion结构体
// 语法格式
message 关键字(相当于被创建出的类的名字)
{
// 成员变量
数据类型 变量名 = 变量的编号; // 编号从1开始, 不能重复
}
// .proto文件 生成 c++ 类的命令
protoc proto文件名 --cpp_out=生成目录
则自己创建一个.proto文件中定义以下数据结构
syntax = "proto3";
message Person
{
int32 id = 1; // 编号从1开始
bytes name = 2;
string sex = 3;
int32 age = 4;
}
3.2 protobuf类的使用
在上面我们在命令行中输入"protoc proto文件名 --cpp_out=生成目录"命令后,就会产生两个protobuf类的文件(.h和.cc)。使用这两个类就可以对数据进行操作
#include <iostream>
#include "Person.pb.h"
using namespace std;
int main()
{
//1.创建对象并且初始化
Person p;
p.set_age(18);
p.set_name( "李四");
p.set_sex("man");
p.set_id(1);
//2.将person数据进行序列号
string output;
p.SerializeToString(&output);
cout << "序列化后的数据:" << output << endl;
//3.数据传输
// 网络传输,设置监听等等然后send数据,此处忽略
//4.接收数据,解析->解码->原始数据
Person pp;
pp.ParseFromString(output);
Info in = pp.info();
//5.处理原始数据 ->打印
cout << "名字为:" << pp.name() << ","<<endl;
cout << "年龄为:" << pp.age() << endl;
cout << "性别为:" << pp.sex() << endl;
cout << "id号:" << pp.id() << endl;
return 0;
}
- set_xxxx:可以对我们的规定的数据赋值
- 调用protobuf类为我们提供的SerializeToString()将我们指定的数据进行序列化,括号内是一个传出参数,即为序列化后的参数
- 序列化后就可以进行网络发送,此处省略
- 获得数据后同样传出对象后调用protobuf类为我们提供的ParseFromString()方法,将序列化好的数据进行发序列化,就可以得到原始数据
3.3 以面向对象思想写protobuf类
// 该结构体主要是方便参数传入
struct Info
{
int id;
string name;
string sex; //
int age;
};
class My_Protobuf
{
public:
// 空构造
My_Protobuf();
// 构造出的对象用于 解码 的场景
My_Protobuf(string enstr) {InitMyProto(enstr);}
// 构造出的对象用于 编码 场景
My_Protobuf(Info* info){InitMyProto(info);}
~My_Protobuf();
// 编码时候使用
void InitMyProto(Info* info)
{
m_person.set_id(info->id);
m_person.set_name(info->name);
m_person.set_sex(info->sex);
m_person.set_age(info->age);
}
// 解码使用
void InitMyProto(string enstr)
{
m_enstr = enstr;
}
// 编码
string encodeMsg()
{
string output;
m_person.SerializeToString(&output);
return output;
}
// 解码
void* decodeMsg()
{
m_person.ParseFromString(m_enstr);
retrun &m_person;
}
private:
//Person这个类由protobuf产生
Person* m_person;
string m_enstr;
}
3.4 protobuf其他用法
1.repeated
repeated bytes name = 2;// name可以在程序中创建多个, 在程序中作为动态数组来使用
2.enum枚举
enum Color
{
Red = 0; // protbuf中第一个枚举值必须为0
Green = 6;
Blue = 9;
}
3.命名空间
package itcast; // 某个类属于itcast这个命名空间
假设在一个protobuf文件中有以下信息,通过命令行会生成6个对应的类,我们将其进行封装
syntax="proto3";
package pb;
message SyncPid
{
int32 Pid = 1;
string Username = 2;
}
message Player
{
int32 Pid = 1;
Position P=2;
string Username = 3;
}
message SyncPlayers
{
/*嵌套多个子消息类型的Player消息*/
repeated Player ps = 1;
}
message Position
{
float X=1;
float Y=2;
float Z=3;
float V=4;
}
message BroadCast
{
int32 Pid=1;
int32 Tp=2;
/*根据Tp不同会选择一下其中一个
content 消息内容
Position 位置
*/
oneof data{
string content=3;
Position P=4;
/*预留*/
int32 ActionData=5;
}
string Username=6;
}
message Talk
{
string Content = 1;
}
- 这里生成的6个类对应的不同的消息类型,所以我们构造的时候需要通过某些标志位构造不同的消息
- 在protobuf的所有消息中都i有一个共同的父类google::protobuf::Message类,因此我们可以通过这个父类指针去获得上面6个类中的任意数据
class MyMsg{
public:
enum MyMsgType {
MY_MSG_LOGON_SYNCPID = 1,
MY_MSG_TALK_CONTENT = 2,
MY_MSG_NEW_POSTION = 3,
MY_MSG_BROADCAST = 200,
MY_MSG_LOGOFF_SYNCPID = 201,
MY_MSG_SUR_PLAYER = 202,
} m_MsgType;
google::protobuf::Message *m_poMsg = NULL;
MyMsg(MyMsgType _Type, google::protobuf::Message * _msg);
MyMsg(MyMsgType _Type, std::string _szInputData);
~MyMsg();
std::string Serialize();
};
- 通过一个枚举值规定不同的数据类型,根据不同数据类型构造不同的消息
- m_poMsg:类的成员,维护一个所有protobuf类的父类指针
- 构造函数
- 构造函数1:通过传进来的类型和子类对象参数,直接初始化m_MsgType和m_poMsg就可以直接获得对应消息
- 构造函数2:根据Type不同new出不同的protobuf对象,然后用创建出来的对象调用ParseFromString方法将参2的string解析获得对应消息
MyMsg::MyMsg(MSG_TYPE _type, google::protobuf::Message* _msg):m_MsgType(_type),m_poMsg (_msg)
{
}
MyMsg::MyMsg(MSG_TYPE type, string str): m_MsgType(type)
{
switch (type)
{
case GameMsg::MSG_TYPE_LOGIN_ID_NAME:
m_Msg = new pb::SyncPid();
break;
case GameMsg::MSG_TYPE_CHAT_CONTENT:
m_Msg = new pb::Talk();
break;
case GameMsg::MSG_TYPE_NEW_POSITION:
m_Msg = new pb::Position();
break;
case GameMsg::MSG_TYPE_BROADCAST:
m_Msg = new pb::BroadCast();
break;
case GameMsg::MSG_TYPE_LOGOFF_ID_NAME:
m_Msg = new pb::SyncPid();
break;
case GameMsg::MSG_TYPE_SRD_POSTION:
m_Msg = new pb::SyncPlayers();
break;
default:
break;
}
m_Msg->ParseFromString(str);
}
string MyMsg::Serialize()
{
string pret;
m_Msg->SerializeToString(&pret);
return pret;
}
MyMsg::~MyMsg()
{
if (m_Msg != nullptr)
{
delete m_Msg;
m_Msg = nullptr;
}
}
标签:const,protobuf,json,Value,Json,序列化,string
From: https://blog.csdn.net/C_J_L12580/article/details/143868458