一、序列化与反序列化概念

上一章讲解了TCP通信【网络编程】demo版TCP网络服务器实现,我们知道TCP是面向字节流的方式进行通信。
在这里插入图片描述
但是这里就会引发一个问题:怎么保证正好就读到一个完整的数据呢?

举个例子:我们使用QQ发送消息的时候别人接收到的不仅仅只有消息,而是包含了头像信息,昵称,消息。这就叫做结构化的数据。这些结构化的数据可以打包成一个报文(变成一个整体),这个过程就叫做序列化。而把这个整体报文解开的过程就叫做反序列化

结构化数据要先序列化再发送到网络中,收到序列字节流后,要先反序列化再使用。

而这里序列化和反序列化的过程用的就是业务协议

二、自定义协议设计网络计算机

2.1 服务端

自定义协议里要包含两各类,一个是请求,一个是响应
服务端会收到请求,客户端收到响应。

// 请求
class Request
{
public:
    
public:
    int _x = 0;
    int _y = 0;
    char _op = 0;
};

// 响应
class Response
{
public:
    int _exitcode = 0;// 退出码
    int _result = 0;// 结果
};

请求就是左操作符、右操作符和符号
响应包含了退出码和结果,如果正常结束退出码为0,如果有错误,我们可以自定义不同的退出码表示不同的错误。

2.1.1 服务端业务处理流程

先来看一下服务端处理数据流程

客户端发过来的数据已经序列化成了一个序列字节流数据(报文),所以服务端首先要先把报文反序列化,构成一个结构化请求对象Request。然后就可以进行计算处理形成一个Response对象,再序列化后发送给客户端。

可以看到计算处理这一步其实跟接收发送消息、序列化与反序列化没什么关系,所以可以把计算处理任务在服务端启动的时候传递进去

计算处理函数:
typedef std::function<bool(const Request& req, Response& resp)> func_t;
这里的req是输入型参数(已经反序列化好的对象),resp是输出型参数,为了获取计算结果。

2.1.2 TCP的发送与接收缓冲区

在这里插入图片描述

我们前面使用的write和read接口并不是直接往网络里发送数据或者从网络里读取数据,write其实是把数据拷贝到传输层的缓冲区,由TCP协议决定什么时候把缓冲区的数据发送到网络中。所以TCP协议也叫传输控制协议
发送数据的本质就是将数据从发送缓冲区拷贝到接收缓冲区。

所以客户端/服务端发送数据不会影响接受数据。
所以TCP是全双工的。

而这就会导致一个问题:可能数据堆积在缓冲区来不及度,一次会读取多个报文挨在一起。那么怎么保证读取完整报文呢?

2.1.3 保证读取完整报文

因为TCP是面向字节流的,所以要明确报文与报文的分界。
为什么要这样呢?举个例子:
现在要把两个数字合并成字符串发送,1、12,如果不处理的话就是"112",这样我们反序列化的时候就不知道到底怎么组合了。
而如果我们在分割的地方加一个符号比如,,序列化后:"1,12",这样就很容易拆分。

保证报文读取完整性的方法:
1️⃣ 定长: 规定长度,每次就读取这么多。
2️⃣ 特殊字符: 就是上面的方法。
3️⃣ 自描述方式: 比如在报文前面带上四个字节的字段,标识报文长度。

2.1.4 自定义协议——序列化与反序列化

先来看请求的序列化与反序列化

2.1.4.1 请求
int _x = 0;
int _y = 0;
char _op = 0;

我们希望序列化成这样:"_x _op _y"

#define SEP " "
#define SEP_LEN strlen(SEP)
#define SEP_LINE "\r\n"
#define SEP_LINE_LEN strlen(SEP_LINE)

// 请求
class Request
{
public:
    Request(int x, int y, char op)
       : _x(x)
       , _y(y)
       , _op(op)
    {}
    
    Request()
    {}

    // 序列化
    bool serialize(std::string* out/*输出型参数*/)
    {
        // "_x _op _y"
        std::string sx = std::to_string(_x);
        std::string sy = std::to_string(_y);
        *out = sx + SEP + _op + SEP + sy;
        return true;
    }

    // 反序列化
    bool deserialize(const std::string& in)
    {
        // "_x _op _y"
        auto lsep = in.find(SEP);
        auto rsep = in.rfind(SEP);
        if(lsep == std::string::npos || rsep == std::string::npos
        || lsep == rsep) return false;
        std::string sx = in.substr(0, lsep);
        std::string sy = in.substr(rsep + SEP_LEN);
        if(sx.empty() || sy.empty()) return false;
        _x = stoi(sx);
        _y = stoi(sy);
        _op = in[lsep + SEP_LEN];
        return true;
    }
public:
    int _x = 0;
    int _y = 0;
    char _op = 0;
};

这里的反序列化我们传进去的字符串已经把"\r\n"去掉了。
先来看响应的序列化与反序列化

2.4.1.2 响应

我们希望序列化成这样:"_exitcode _result"

// 响应
class Response
{
public:
    Response(int exitcode, int result)
        : _exitcode(exitcode)
        , _result(result)
    {}

    Response()
    {}

    // 序列化
    bool serialize(std::string* out/*输出型参数*/)
    {
        std::string se = std::to_string(_exitcode);
        std::string sr = std::to_string(_result);
        *out = se + SEP + sr;
        return true;
    }
    
    // 反序列化
    bool deserialize(const std::string& in)
    {
        // "_exitcode _result"
        auto pos = in.find(SEP);
        if(pos == std::string::npos) return false;
        std::string se = in.substr(0, pos);
        std::string sr = in.substr(pos + SEP_LEN);
        if(se.empty() || sr.empty()) return false;
        _exitcode = stoi(se);
        _result = stoi(sr);
        return true;
    }
public:
    int _exitcode = 0;// 退出码
    int _result = 0;// 结果
};

2.1.5 计算流程

计算结果会形成一个resp响应,里面包含了退出码,我们可以自己设置退出码数值含义:

enum {
    OK,
    DIV_ZERO,
    OP_ERROR
};

计算逻辑:

std::unordered_map<char, std::function<int(int, int)>> hash = 
{
    {'+', [](int x, int y)->int{return x + y;}},
    {'-', [](int x, int y)->int{return x - y;}},
    {'*', [](int x, int y)->int{return x * y;}},
    {'/', [](int x, int y)->int{return x / y;}},
    {'%', [](int x, int y)->int{return x % y;}},
};

bool calc(const Request& req, Response& resp)
{
    // req已经反序列化好了
    if(!hash.count(req._op)) 
    {
        resp._exitcode = OP_ERROR;
        return false;
    }
    if(req._op == '/' || req._op == '%')
    {
        if(req._y == 0)
        {
            resp._exitcode = DIV_ZERO;
            return false;
        }
    }
    resp._result = hash[req._op](req._x, req._y);
    return true;
}

2.1.6 在有效载荷前添加长度报头

在这里插入图片描述

"_x _op _y" -> "content_len\r\n_x _op _y\r\n"
"_exitcode _result" -> "content_len\r\n_exitcode _result\r\n"
// 给有效载荷添加报头信息
std::string enLength(const std::string& text)
{
    std::string send_str = std::to_string(text.size());
    send_str += SEP_LINE + text + SEP_LINE;
    return send_str;
}  

2.1.7 发送响应send

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

RETURN VALUE
On success, these calls return the number of characters sent.  
On error, -1 is returned, and errno is set appropriately.

服务端收到请求到把响应发送出去的整个流程:

// 处理请求的入口
void handler(int sock, func_t func)
{
    // 得到序列化好的请求对象
    
    std::string req_str;
    // 得到结构化请求对象
    Request req;
    if(!req.deserialize(req_str)) return;
    // 计算,得到响应
    Response resp;
    func(req, resp);
    // 序列化响应
    std::string resp_str;
    resp.serialize(&resp_str);
    // 添加报头
    std::string send_str = enLength(resp_str);
    // 发送响应
    send(sock, send_str.c_str(), send_str.size(), 0);
}

那么这里的第一步是怎么读取请求的呢?
这个请求必须是恰好一个完整的请求。

2.1.8 读取一个完整的报文recv

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

RETURN VALUE
These  calls return the number of bytes received, or -1 if an error occurred.  In the event of an error, errno is set to indicate the error.  The
return value will be 0 when the peer has performed an orderly shutdown.
// "content_len\r\n_x _op _y\r\n"
// 读取一个完整报文
bool recvPackage(int sock, std::string& inbuf, std::string* out)
{
    char buf[1024];
    while(true)
    {
        ssize_t n = recv(sock, buf, sizeof buf - 1, 0);
        if(n > 0)
        {
            buf[n] = '\0';
            inbuf += buf;
            auto pos = inbuf.find(SEP_LINE);
            if(pos == std::string::npos) continue;// 还得继续读取
            std::string text_len = inbuf.substr(0, pos);
            int content_len = stoi(text_len);
            int total_len = text_len.size() + 2 * SEP_LINE_LEN + content_len;// 一个完整报文长度
            if(inbuf.size() < total_len) continue;// 还得继续读取
            // 至少有一个完整报文
            *out = inbuf.substr(0, total_len);
            inbuf.erase(0, total_len);
            return true;
        }
        else return false;
    }
    return true;
}

收到的请求还需要去掉报头

// 去掉有效载荷的报头信息
bool deLength(const std::string& pack, std::string *out)
{
    auto pos = pack.find(SEP_LINE);
    if(pos == std::string::npos) return false;
    std::string text_len_string = pack.substr(0, pos);
    int text_len = stoi(text_len_string);
    *out = pack.substr(pos + SEP_LINE_LEN, text_len);
    return true;
}

这样服务端的业务逻辑就完成了:

// 处理请求的入口
void handler(int sock, func_t func)
{
    std::string inbuf;// 输入缓冲区
    while(1)
    {
        // 得到序列化好的请求对象
        std::string req_text;
        if(!recvPackage(sock, inbuf, &req_text)) return;
        std::string req_str;
        if(!deLength(req_text, &req_str)) return;
        // 得到结构化请求对象
        Request req;
        if(!req.deserialize(req_str)) return;
        // 计算,得到响应
        Response resp;
        func(req, resp);
        // 序列化响应
        std::string resp_str;
        resp.serialize(&resp_str);
        // 添加报头
        std::string send_str = enLength(resp_str);
        // 发送响应
        send(sock, send_str.c_str(), send_str.size(), 0);
    }
}

2.2 客户端

大致流程跟服务端差不多:

void start()
{
    struct sockaddr_in si;
    bzero(&si, sizeof si);
    si.sin_family = AF_INET;
    si.sin_port = htons(_serverport);
    si.sin_addr.s_addr = inet_addr(_serverip.c_str());
    if(connect(_sock, (struct sockaddr*)&si, sizeof si) < 0)
    {
        std::cout << "connect socket error" << std::endl;
    }
    else
    {
        std::string msg;
        std::string inbuf;// 输入缓冲区
        while(1)
        {
            std::cout << "Please Enter#";
            std::getline(std::cin, msg);// 1+2
            // 解析字符串
            Request req = PraseMsg(msg);
            // 序列化
            std::string content;
            req.serialize(&content);
            // 添加报头
            std::string send_str = enLength(content);
            // 发送
            send(_sock, send_str.c_str(), send_str.size(), 0);
            // 获取响应结果
            std::string package;
            // "content_len\r\n_x _op _y\r\n"
            if(!recvPackage(_sock, inbuf, &package)) continue;// 还要继续读
            // 去掉报头,提取正文
            std::string text;
            if(!deLength(package, &text)) continue;
            // 反序列化获取退出码和结果
            Response resp;
            resp.deserialize(text);
            std::cout << "exitcode: " << resp._exitcode << std::endl;
            std::cout << "result: " << resp._result << std::endl;
        }
    }
}

// 解析字符串
Request PraseMsg(const std::string& msg)
{
    // "123+456"
    int idx_op = 0;
    int idx = 0, n = msg.size();
    // 找符号位置
    while(idx < n)
    {
        if(hash.count(msg[idx]))
        {
            idx_op = idx;
            break;
        }
        idx++;
    }
    Request req;
    std::string sx = msg.substr(0, idx_op);
    std::string sy = msg.substr(idx_op + 1);
    req._x = stoi(sx);
    req._y = stoi(sy);
    req._op = msg[idx_op];
    return req;
}

流程就是序列化请求,添加报头,发送,接收响应,去掉报头,反序列化,获取结果。

2.3 结果

客户端:
在这里插入图片描述
服务端:
在这里插入图片描述

三、使用Json进行序列化和反序列化

序列化与反序列化其实C++提供了Json的库。我们可以直接使用:

Json(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于Web应用程序中的数据传输。它是一种基于文本的格式,易于读写和解析。Json格式的数据可以被多种编程语言支持,包括JavaScript、Python、Java、C#、C++等。Json数据由键值对组成,使用大括号表示对象,使用方括号表示数组。

首先先安装Json库。

sudo yum install -y jsoncpp-devel

头文件:#include <jsoncpp/json/json.h>

使用jsoncpp库记得在编译时加上-ljsoncpp

Makefile:

.PHONY:all
all:CalcServer CalcClient

CalcClient:CalcClient.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
CalcServer:CalcServer.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF

.PHONY:clean
clean:
	rm -f CalcClient CalcServer
// 请求
class Request
{
public:
    Request(int x, int y, char op)
       : _x(x)
       , _y(y)
       , _op(op)
    {}

    Request()
    {}

    // 序列化
    bool serialize(std::string* out/*输出型参数*/)
    {
#ifdef MYSELF
        // "_x _op _y"
        std::string sx = std::to_string(_x);
        std::string sy = std::to_string(_y);
        *out = sx + SEP + _op + SEP + sy;
#else
        Json::Value root;// 万能对象,可接收任何对象
        root["first"] = _x;// 自动将_x转换为字符串
        root["second"] = _y;
        root["oper"] = _op;
        // 序列化
        Json::FastWriter writer;
        *out = writer.write(root);// 将root进行序列化
#endif
        return true;
    }

    // 反序列化
    bool deserialize(const std::string& in)
    {
#ifdef MYSELF
        // "_x _op _y"
        auto lsep = in.find(SEP);
        auto rsep = in.rfind(SEP);
        if(lsep == std::string::npos || rsep == std::string::npos
        || lsep == rsep) return false;
        std::string sx = in.substr(0, lsep);
        std::string sy = in.substr(rsep + SEP_LEN);
        if(sx.empty() || sy.empty()) return false;
        _x = stoi(sx);
        _y = stoi(sy);
        _op = in[lsep + SEP_LEN];
#else
        //Json反序列化
        Json::Value root;// 万能对象,可接收任何对象
        Json::Reader reader;
        reader.parse(in,root);// 第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中
        //反序列化
        _x = root["first"].asInt();// 默认是字符串,转换为整型
        _y = root["second"].asInt();
        _op = root["oper"].asInt();// 转换为整型,整型可以给char类型
#endif
        return true;
    }
public:
    int _x = 0;
    int _y = 0;
    char _op = 0;
};

// 响应
class Response
{
public:
    Response(int exitcode, int result)
        : _exitcode(exitcode)
        , _result(result)
    {}

    Response()
    {}

    // 序列化
    bool serialize(std::string* out/*输出型参数*/)
    {
#ifdef MYSELF
        std::string se = std::to_string(_exitcode);
        std::string sr = std::to_string(_result);
        *out = se + SEP + sr;
#else
        Json::Value root;// 万能对象,可接收任何对象
        root["exitcode"] = _exitcode;// 自动将_exit转换为字符串
        root["result"] = _result;
        // 序列化
        Json::FastWriter writer;
        *out = writer.write(root);// 将root进行序列化
#endif
        return true;
    }
    
    // 反序列化
    bool deserialize(const std::string& in)
    {
        // "_exitcode _result"
#ifdef MYSELF
        auto pos = in.find(SEP);
        if(pos == std::string::npos) return false;
        std::string se = in.substr(0, pos);
        std::string sr = in.substr(pos + SEP_LEN);
        if(se.empty() || sr.empty()) return false;
        _exitcode = stoi(se);
        _result = stoi(sr);
#else
        //Json反序列化
        Json::Value root;// 万能对象,可接收任何对象
        Json::Reader reader;
        reader.parse(in,root);// 第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中
        //反序列化
        _exitcode = root["exitcode"].asInt();// 默认是字符串,转换为整型
        _result = root["result"].asInt();
#endif
        return true;
    }
public:
    int _exitcode = 0;// 退出码
    int _result = 0;// 结果
};


更多推荐