自学内容网 自学内容网

Linux网络——自定义协议与序列化

一、协议

协议是一种 " 约定 ". socket api 的接口 , 在读写数据时 , 都是按 " 字符串 " 的方式来发送接收的。如
果我们要传输一些 " 结构化的数据 ",依然可以通过协议。
其实,协议就是双方约定好的结构化的数据。

二、网络计算器

假如说,想要制作一个网络计算器,我们就需要给出特定的协议,让客户端发出的运算条件能够被服务端接收并计算再进行返回。

我们已经了解,协议就是通信双方约定好的结构化数据,所以我们自定义协议,就可以通过结构体来实现,例如自制一个专用于加减乘除取模的计算器,我们制定如下协议:

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库的方法:

ubuntusudo 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通信时,我们只需要将要发送数据先序列化并添加报头,得到完整的报文,再在接收数据时,去除报头,进而将报文反序列化,从而得到数据



原文地址:https://blog.csdn.net/2303_78442132/article/details/143184006

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!