序言
在上一篇文章中,我们介绍了Socket 编程,已经可以简单地使用该方法来进行服务端和客户端的数据了。在这篇文章中我们将在此基础上学习序列化和反序列化,以及在应用层上自定义协议。
序列化和反序列化
1. 为什么需要序列化和反序列化?
在 Socket 编程
的学习中,客户端给服务端传输的数据全是字符串,但是如果我们有传输对象或者是结构体
的需求怎么办呢?
首先我们需要知道在网络通信中,直接传输对象或复杂数据结构通常是不可行的
,因为网络协议通常只支持字节流或字符流的传输。所以想要传输结构体我们就首先需要将该结构体或对象序列化!序列化可以将这些对象或数据结构转换为字节序列(如二进制格式)或字符序列(如JSON、XML
等),从而在网络中传输。
2. 安装 Json 库
Jsoncpp
是一个用于处理 JSON
数据的 C++
库。它提供了将 JSON 数据序列化为字 符串以及从字符串反序列化为 C++ 数据结构的功能
。Jsoncpp
是开源的,广泛用于各种需要处理 JSON
数据的 C++
项目中。
我们在这里将简单的带大家上手这个库,首先我们需要安装这个库,在 Linux
输入指令:
ubuntu:sudo apt-get install libjsoncpp-dev
3. 数据序列化
我们构造一个简单的结构体只包含内置类型的数据:
struct MyData
{
MyData(int left, int right, char oper)
: _left(left)
, _right(right)
, _oper(oper)
{}
int _left;
int _right;
char _oper;
};
现在我们初始化一个结构体对象,Json::Value对象
,并使用结构体的数据填充该Json
:
// 初始化一个对象
MyData d1(1, 2, '-');
// 创建一个 Json::Value 对象,它将用于构建 JSON
Json::Value root;
// 填充 Json 对象
root["Left"] = d1._left;
root["Right"] = d1._right;
root["Oper"] = d1._oper;
JSON
已经构建好了,现在我们只需要格式化输出字符串就行了:
// FastWriter 的效率比较好,因为不需要额外的输出空格,换行符
Json::FastWriter write;
std::string result = write.write(root);
我们输出一下序列化的结果:
{"Left":1,"Oper":45,"Right":2}
在这里我们也尝试一下其他的格式化输出字符串,比如:
std::string result = root.toStyledString();
输出带有一定风格的字符串结果:
{
"Left" : 1,
"Oper" : 45,
"Right" : 2
}
如果我使用的是 class
就不能直接访问类的私有成员变量,所以为了更好地实现类的封装,我觉得在类中实现一个成员函数来帮我们完成是更好的方案:
Json::Value toJson() const
{
// 创建一个 Json::Value 对象,它将用于构建 JSON
Json::Value root;
// 填充 Json 对象
root["Left"] = _left;
root["Right"] = _right;
root["Oper"] = _oper;
return root;
}
现在我们已经将我们的结构体输出为字符串了,并假设已经发送给服务端了,那服务端如何反序列化呢得到正确的数据呢?
4. 数据反序列化
将数据序列化传送到服务端不是目的,服务端完整的获取原格式的数据才是最终目的!现在我们就需要将数据反序列化,首先我们初始化一个 Json::Value
来存储解析后的 Json
数据,其次我们还要一个 Json::Reader
来解析数据:
Json::Value root;
Json::Reader read;
// 解析数据到 root 中
read.parse(result, root);
我们将获取的数据再构造一个对象也就实现了 脱胎换骨
:
MyData d2(root["Left"].asInt(), root["Right"].asInt(), root["Oper"].asInt());
在这里需要注意,我们 提取数据时一定要指定数据的类型
!
自定义协议
1. 再识 TCP
TCP 是一个全双工通信,他在接收消息的同时也可以发送信息!则会是因为 TCP 在底层实现中 包含两个主要的缓冲区
:一个用于发送数据(发送缓冲区),另一个用于接收数据(接收缓冲区):
如果存在一个场景,发送端发送信息的次数极其高,但是接收端的处理数据的速度也很有限,这就会造成接受端的接收缓冲区非常的拥挤!当我们的发送端发送一个完整的报文过去时,有没有可能我们的接收端没有充足的空间来接受呢?这是完全有可能的吧,这就会造成报文的部分信息丢失!
为了解决这个问题,我们在应用层可以自定义一个协议,该协议用于判断数据是否完整!
注意:UDP也是一个全双工通信,但是在真正意义上他并不具有发送缓冲区!因为UDP是无连接的、不可靠的协议,它不会将数据保存在缓冲区中等待确认发送!他会直接将数据加上报头后发送!
2. 自定义协议 — 发送端
我们在这里需要编写一个程序,该程序需要发送一个人的信息给服务端,服务端接收后如果信息完整,返回 OK。该结构体的内容包括如下:
class Person
{
public:
Person(std::string name, int age, int weight)
: _name(name)
, _age(age)
, _height(weight)
{}
// 便于直接使用 Json 对象初始化
Person(Json::Value& root)
: _name(root["Name"].asString())
, _age(root["Age"].asInt())
, _height(root["Height"].asInt())
{}
// 序列化
Json::Value toJson()
{
Json::Value root;
root["Name"] = _name;
root["Age"] = _age;
root["Height"] = _height;
return root;
}
private:
std::string _name;
int _age;
int _height;
};
增添的 toJson
功能以及使用 Json 对象
初始化等会会用到,一定程度方便我们的编写。
现在我们将我们的数据序列化了,那如何让添加自定义协议呢?在这里我们的报头就简单一些,我们只是 添加序列化后字符串的长度
:
len\r\nResult\r\n
在这里我们的 \r\n
代表分隔符,隔开长度和字符串,也隔开不同的组,现在我们就来实现该函数吧:
const std::string SEP = "\r\n";
// 添加报头
void Encode(std::string& jsonstr)
{
int len = jsonstr.size();
jsonstr = std::to_string(len) + SEP + jsonstr + SEP;
}
现在我们看一下加上报头后,我们的序列化后的数据是啥样:
38
{"Age":12,"Height":175,"Name":"jack"}
符合我们的预期!
3. 自定义协议 — 接收端
加入我们现在已经接收到了发送端的数据,那该如何还原呢?现在不能之间反序列化了,因为我们在序列化的基础上加上了报头,那我们先去报头查看信息是否完整:
// 去除报头
std::string Decode(std::string& packagestream)
{
auto pos = packagestream.find(SEP);
if(pos == std::string::npos) return std::string();
// 获取长度
std::string lenstr = packagestream.substr(0, pos);
int len = std::stoi(lenstr);
// 判断信息是否完整 len\r\nJsonStr\r\n
int totalsize = lenstr.size() + SEP.size() + len + SEP.size();
if(packagestream.size() < totalsize) return std::string();
std::string jsonstr = packagestream.substr(pos + SEP.size(), len);
return jsonstr;
}
在这里主要就设计字符串的分割操作,理解了也是很简单的,现在我们只需要将该字符串解析为 Json
对象就好啦:
jsonstr = Decode(jsonstr);
Json::Reader().parse(jsonstr, root);
Person p1(root);
我们在前面完成的 Person
构造函数也发挥了作用!
总结
在这篇文章中我们自己实现了对数据的序列化和反序列化,并且简单构造了在应用层的协议,希望大家有所收获!