一、协议
二、网络计算器
假如说,想要制作一个网络计算器,我们就需要给出特定的协议,让客户端发出的运算条件能够被服务端接收并计算再进行返回。
我们已经了解,协议就是通信双方约定好的结构化数据,所以我们自定义协议,就可以通过结构体来实现,例如自制一个专用于加减乘除取模的计算器,我们制定如下协议:
struct Request
{
int x;
int y;
char oper;//+ - * / %
};
在该协议中,x只用于第一个运算数,y只用于第二个运算数,oper为运算符。
struct Response
{
int result;
int code;//0:success 1:dev zero 2.非法操作
};
在该协议中,result为运算结果,code表示运算情况,0表示成功运算,1为除0错误,2为非法使用其他运算符。
但是仅仅有了上述结构体,就可以实现用户与服务器的完美通信了吗,并不能。
我们的服务器是Linux系统,但是客户端呢?一定也是Linux系统吗?当然不一定,客户端可以是windows,安卓以及iOS,甚至客户端和服务端所使用的编程语言也不相同,更重要的是,网络通信是以字节流的方式,我们并不能直接传递结构体数据。
那么解决这一问题,可以通过下述方法:
1.序列化和反序列化
在上述结构体中,一个运算是由结构体的三部分共同构成的,而序列化,就是将这三部分整合成一个字符串,即"x oper y"这样一个字符串,三部分之间通过自己规定的字符隔开,比如空格,这样我们就把一个结构体转换成一个字符串,通过网络传输之后,再在接受方将字符串进行分割,重新组成结构体,即反序列化。
在库中,封装了很多能够实现序列化和反序列化的工具,包括xml、json、protobuf等等,其中json是c++标准库所封装的,所以本篇文章我们就来分享json实现序列化反序列化。
2.Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。
下面是Linux两种不同环境下,按照json库的方法:
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
3.序列化
因为刚下载的json库,并没有进行链接,所以使用json库需要包含头文件:
#include <jsoncpp/json/json.h>
下面我们将上边给出的Request类进行序列化,来看代码:
//序列化
void Serialize(string *out)
{
//1.使用现成的库
Json::Value root;
root["x"] = _x;
root["y"] = _x;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
}
int main()
{
Request req(1,2,'+');
string out;
req.Serialize(&out);
cout << out << endl;
return 0;
}
定义一个Json库中的Value对象,该对象中重载了“[]”,通过键值对的方式,将成员变量与其对应的值捆绑,并记录在root对象中,紧接着定义Json库中的FastWriter对象,其中的write函数,能够将Value对象中存放的键值对转换为字符串并返回。
结果如下:
该字符串就是通过Json库序列化之后形成的,称为Json串。
当然除了普通的内置类型数据,Json还可以序列化Value对象自己,以及数组等各种类型的数据。
4.反序列化
直接来看代码:
// 反序列化
void Deserialize(const string &in)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
cout << _x << ' ' << _y << ' ' << _oper << endl;
}
int main()
{
string in = "{\"oper\":43,\"x\":1,\"y\":1}";
Request req;
req.Deserialize(in);
return 0;
}
反序列化,需要定义Json类中的Reader对象,调用其中的parse函数,传入要反序列化的Json串,以及Value对象,用于接收反序列化后的键值对数据。随后,通过asInt()函数,将数据以整型方式获取,注意单字符也是整型,后续通过ASCLL码转换。
结果如下:
5.设计协议报头
单单进行序列化,是无法满足直接通过网络进行传输的,因为在传输过程中,可能出现阻塞,导致最终可能无法得到完整的数据序列。所以我们还需要给协议添加报头,使得得到的整个报文格式完整,这里我们设计报文格式为:
"len"\r\n"{json}"\r\n
- len:表示json串有效载荷的长度。
- 中间\r\n:用于区分len和json串。
- 结尾\r\n:暂时无用,方便debug。
具体方法如下:
static const string sep = "\r\n";
//添加报头
string Encode(const string &jsonstr)
{
int len = jsonstr.size();
string strlen = to_string(len);
return strlen + sep + jsonstr + sep;
}
添加报头较为简单,获取到json串的长度,转为string类型,在进行拼接即可。
//拆解报头
string Decode(string &packagestream)
{
//是否拥有完整的中间sep
auto pos = packagestream.find(sep);
if(pos == string::npos) return string();
//获得json串长度
string strlen = packagestream.substr(0,pos);
int len = stoi(strlen);
//得到报文完整长度
int total = strlen.size() + len + 2 * sep.size();
if(packagestream.size() < total) return string();
//得到json串
string jsonstr = packagestream.substr(pos + sep.size(),len);
//将得到的json串从原数据流中删除
packagestream.erase(total);
return jsonstr;
}
在拆解报头中,首先要进行判断,该数据流是否包含完整的中间sep,不包含说明数据不全,不做拆解;进而通过json串的长度,能够计算出整个报文的长度,判断数据流的长度是否小于整个报文的长度,如果小于,则说明没有完整的报文,不做拆解;如果大于,说明包含完整的报文,就可以进行拆解,得到json串,最后将拆解的报文从原数据流中删除。
随后在进行TCP/UDP通信时,我们只需要将要发送数据先序列化并添加报头,得到完整的报文,再在接收数据时,去除报头,进而将报文反序列化,从而得到数据。