【Linux | 网络编程】序列化与反序列化

📖 前言

本章我们将重谈网络协议,了解主流的网络协议,学习序列化与反序列化,并通过自己手写一个简易版的网络协议,来了解网络传输中数据的变化过程。目标已经确定,办好小板凳,准备开讲啦……

在学习本章内容之前,先复习之前的👉 TcpServer
本章代码详情:👉 Gitee


1. 重谈协议

网络协议:

  • 网络中的协议是指在计算机网络中,用于规定数据传输和通信过程中的约定和规则。
  • 协议定义了数据的格式、传输方式、错误处理等细节,以确保不同设备或系统之间能够正确地进行通信。

协议是一批数据和逻辑的结合。网络协议的分层,无外乎是数据结构和逻辑的分层。协议一封层方便我们去维护,因为能很好的做到了逻辑解耦。

1.1 网络协议存在的意义:

  1. 数据传输标准化: 网络协议定义了数据在网络中的传输格式和规范,确保不同计算机和设备之间能够相互通信。通过统一的协议,使得各种不同类型和品牌的设备可以互相交流和协作。
  2. 数据分割与重组: 网络协议将大块的数据分割成小的数据包进行传输,同时保证数据包的正确顺序和完整性。这样可以提高数据传输的效率和可靠性,减少网络拥塞和错误。
  3. 错误检测和纠正: 网络协议通过添加校验位或冗余数据,可以检测和纠正在传输过程中产生的错误。这样可以确保数据的准确性和完整性,提高网络传输的可靠性。
  4. 路由和寻址: 网络协议定义了数据在网络中的路由方式和寻址方式,即确定数据从发送方到达接收方所需经过的路径和目标地址。通过协议规定的路由和寻址机制,网络可以有效地将数据传送到指定的目的地。
  5. 安全性和隐私保护: 网络协议可以支持加密和身份验证等安全机制,确保数据在传输过程中的安全性和隐私保护。这对于保护用户的敏感信息和防止网络攻击至关重要。

1.2 信息发送:

在我们前几章的内容学习中,我们在干什么呢?之前在做的事情:造轮子,学习网路通信,定制化服务。我们做的所有的工作都是在原生的编写应用层代码。

我们之前写的应用层没有定协议,也可以理解为发字符串是定协议,但是这个协议定制的不明显。

  • 当我们需要发送结构化数据该怎么办?

TCP是面向字节流的,也就是其能够发送任意数据,就能够发送结构体的二进制数据:

  • 假设收到的消息在buffer里面,将指针强转之后直接读取。
  • 大部分情况下是可以的,因为网络服务器就是这么定制的,就是按照字节来定的。
  • 仅仅是网络协议是可以,我们可别这么干,而且绝对不能这么干。

因为网络协议考虑到了各种问题:

  • 大小端问题,和结构体的大小在不同对齐数下是不一样的。
  • 有可能服务端在Linux下,客户端在Windows下,两个对同一个结构体的大小计算可能不一样。
  • 编译器也可能不同。

这些情况的出现,可能会导致传输的数据的丢失,这是我们不愿意看到的。

所以直接传结构体这种方案是绝对不行的!!!


2. 序列化与反序列化

直接发送结构体对象是不可取的,虽然在某些情况下它确实行,但是我们是不会采取这种办法传输数据的。

我们需要进行序列化和反序列化。

序列化(Serialization)是指将数据结构或对象转换为字节流的过程:

  • 在序列化过程中,数据结构中的字段和属性被转换为字节的形式,可以包括整型、浮点型、字符串等数据类型。
  • 这个字节流可以被存储到文件系统、数据库中,也可以通过网络传输给其他系统。
  • 序列化的结果是一个字节序列,可以实现数据的持久化存储或跨网络传输。

反序列化(Deserialization)是指将字节流还原为原始数据结构或对象的过程:

  • 在反序列化过程中,字节流被解析,并将其中的数据重新构建为与序列化前相同的结构。
  • 这样就可以恢复数据的完整性和原始形态,进而进行后续的处理、读取或展示。

TCP,UDP在通信是底层用的就是结构体定好了的,但是通常学习工作中,我们要用一些其他的东西,组件或者框架,或者其他方式,完成序列化和反序列化,在应用层。如果愿意的话,也可以用结构体定自己的协议,但是太麻烦了。
借助编程语言提供的序列化库来实现,使用第三方库如Jackson、Gson等来实现对象的序列化和反序列化。

举个栗子:

struct Date
{
	int Length;
	int Width;
	int Height;
};

序列化:要想将其序列化,可以用一个简单的方式将其拼接成一个字符串。

Length/:Width/:Height

反序列化:客户端在收到这个字符串之后,通过查找分隔符/:的方式,将三个变量提取出来,在本地转回所需要的结构体。

我们自定义了一个序列化和反序列化的方式,也就是制定了一个简单的的协议!

2.1 编码与解码:

TCP是面向字节流的,因为有可能同时读到多个信息,所以要单独的区分一个子串。

字符串整体的长度对方怎么知道?

  • 我们需要定制协议的时候,序列化之后,需要将长度,设置为4字节。
  • 并且将长度放入序列化之后的字符串的开始之前!

序列化之后的字符串多长呢,要在最前面携带长度有两种方案:

  • 第一种,四字节定长,直接写二进制记录大小,当对方读取时,必定先读取前四个字节,读到整个有效字符串有多长。
  • 第二种,采用字符串风格的strlen,然后加\r\n作区分,然后再和后面的字符串耦合起来,"strlen\r\n "XXXXXXXXXX\r\n

我们采用第二种方式,因为第一种方式的可读性不好。

序列化的字符串和序列化的字符串之间用\r\n就好了,为什么还要读取前面的长度呢?

  • 原因就是我们不能保证序列化之后的字符串内部不包含\r\n
  • 加长度的方案是二进制安全的做法,也是最合适的做法。

定的就叫做应用层协议:

在这里插入图片描述
添加字节数的动作叫做:encode 编码。

客户端接收到序列化的数据后,先根据报头取出前strlen个字节,读取到有效载荷长度后,再往后读取出完整的有效载荷。

对方在读的时候叫做: decode 解码。


3. 网络计算器

3.1 自定义协议:

我们只是简单的实现一个网络计算器,不涉及到负载运算,目的在于学习了解制定协议的过程。

我们实现的是简单的,两个操作数之间的运算:

1+1
2*3
10-7
5/9
// ...

我们规定,序列化之后的字符串格式是,操作数和操作符之间带有空格:

x + y 

规定,编码之后的格式是,报头(长度)和字符之间用\r\n分隔,有效载荷最后用\r\n结尾:

5\r\n1 + 1\r\n
7\r\n10 + 20\r\n

对于处理结果,我们需要有两个变量(退出状态,运算结果):

exitCode_ result_

同时要想将处理结果发送出去,也要进行序列化和编码:

数据长度\r\nexitCode_ result_\r\n

此时我们就定制完成了自己的一套协议。

3.2 实现编码与解码:

把协议中的分隔符给定义出来,方便以后统一使用或更改:

#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

#define OPS "+-*/%"

注意:
在这里插入图片描述
因为sizeof会把\0也算进去。

验证:

在这里插入图片描述

  • 解码:

在解码之前,我们要做一些明确:

  • 必须具有完整的长度。
  • 必须具有和报头相符合的有效载荷。
  • 我们才返回有效载荷和报头。
  • 否则,我们这里实现的就是一个检测函数!
// 9\r\n100 + 200\r\n    9\r\n112 / 200\r\n
std::string decode(std::string &in, uint32_t *len)
{
    assert(len);
    // 1. 确认是否是一个包含len的有效字符串
    *len = 0;
    std::size_t pos = in.find(CRLF);
    if (pos == std::string::npos)
        return ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)

    // 2. 提取长度
    std::string inLen = in.substr(0, pos);
    int intLen = atoi(inLen.c_str());

    // 3. 确认有效载荷也是符合要求的
    int surplus = in.size() - 2 * CRLF_LEN - pos;
    if (surplus < intLen)
        return "";

    // 4. 确认有完整的报文结构
    std::string package = in.substr(pos + CRLF_LEN, intLen);
    *len = intLen;

    // 5. 将当前报文完整的从in中全部移除掉
    int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
    in.erase(0, removeLen);
    
    // 6. 正常返回
    return package;
}

这些操作都是之前语言的功底,就不再过多的赘述。

  • 编码:
// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
    // "exitCode_ result_"
    // "len\r\n""exitCode_ result_\r\n"
    std::string encodein = std::to_string(len);
    encodein += CRLF;
    encodein += in;
    encodein += CRLF;

    return encodein;
}

3.3 安装json库:

我们不仅可以自己制定协议,还可以使用第三方库,这是我们以后最常用的办法,而自己制定协议只是熟悉过程,加强理解。

接下来我们来安装json库,并在我们的代码中将我们自己定制的协议和第三方库提供的协议,两种方案来进行序列化和反序列化。

在这里插入图片描述
查看安装的json:

在这里插入图片描述

3.4 Request 请求:

  • 成员变量:
// 定制的请求 x_ op y_
class Request
{
public:
    // ...
public:
    // 需要计算的数据
    int x_;
    int y_;
    // 需要进行的计算种类
    char op_; // + - * / %
};
  • 序列化:
// 序列化 -- 结构化的数据 -> 字符串
// 认为结构化字段中的内容已经被填充
void serialize(std::string *out)
{
#ifdef MY_SELF
    std::string xstr = std::to_string(x_);
    std::string ystr = std::to_string(y_);
    // std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->

    *out = xstr;
    *out += SPACE;
    *out += op_;
    *out += SPACE;
    *out += ystr;
#else
    //json
    // 1. Value对象,万能对象
    // 2. json是基于KV
    // 3. json有两套操作方法
    // 4. 序列化的时候,会将所有的数据内容,转换成为字符串
    Json::Value root;
    root["x"] = x_;
    root["y"] = y_;
    root["op"] = op_;

    Json::FastWriter fw;
    // Json::StyledWriter fw;
    *out = fw.write(root);
#endif
}

3.4 - 1 使用json的注意事项

我们将自己定制的协议和json一并使用起来,有两种方案,首先我们先定义一个宏:

#define MY_SELF 1

如果我们定义了MY_SELF那么就用自己的定制的序列化方案,否则就用json提供的。

json本身就是一个KV的方案:

在这里插入图片描述
注意,我们要链接第三方库,不然会编译失败:

在这里插入图片描述
如上所示就是链接成功了,👉 动静态库复习

3.4 - 2 编译指令

除了上述通过宏定义来选择调用序列化的方式,还可以通过编译指令来进行选择,下面我们修改一下makefile
在这里插入图片描述

  • 反序列化:
// 反序列化 -- 字符串 -> 结构化的数据
bool deserialize(std::string &in)
{
#ifdef MY_SELF
    // 100 + 200
    std::size_t spaceOne = in.find(SPACE);
    if (std::string::npos == spaceOne)
        return false;
    std::size_t spaceTwo = in.rfind(SPACE);
    if (std::string::npos == spaceTwo)
        return false;

    std::string dataOne = in.substr(0, spaceOne);
    std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
    std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
    if (oper.size() != 1)
        return false;

    // 转成内部成员
    x_ = atoi(dataOne.c_str());
    y_ = atoi(dataTwo.c_str());
    op_ = oper[0];

    return true;
#else
    //json
    Json::Value root;
    Json::Reader rd;
    rd.parse(in, root);
    x_ = root["x"].asInt();
    y_ = root["y"].asInt();
    op_ = root["op"].asInt(); // char本身就是整数

    return true;
#endif
}

把控好字符的下标,就能控制的很OK了,很考验基本功。

  • debug:
void debug()
{
    std::cout << "#################################" << std::endl;
    std::cout << "x_: " << x_ << std::endl;
    std::cout << "op_: " << op_ << std::endl;
    std::cout << "y_: " << y_ << std::endl;
    std::cout << "#################################" << std::endl;
}

3.5 Response 响应:

  • 成员变量:
// 定制的响应
class Response
{
public:
  // ...
public:
    // 退出状态,0标识运算结果合法,非0标识运行结果是非法的,非0是几就表示是什么原因错了!
    int exitCode_;
    // 运算结果
    int result_;
};
  • 序列化:
// 序列化 -- 不仅仅是在网络中应用,本地也是可以直接使用的!
void serialize(std::string *out)
{
#ifdef MY_SELF
    // "exitCode_ result_"
    std::string ec = std::to_string(exitCode_);
    std::string res = std::to_string(result_);

    *out = ec;
    *out += SPACE;
    *out += res;
#else
    //json
    Json::Value root;
    root["exitcode"] = exitCode_;
    root["result"] = result_;
    Json::FastWriter fw;
    // Json::StyledWriter fw;
    *out = fw.write(root);
#endif
}
  • 反序列化:
// 反序列化
bool deserialize(std::string &in)
{
#ifdef MY_SELF
    // "0 100"
    std::size_t pos = in.find(SPACE);
    if (std::string::npos == pos)
        return false;
    std::string codestr = in.substr(0, pos);
    std::string reststr = in.substr(pos + SPACE_LEN);

    // 将反序列化的结果写入到内部成员中,形成结构化数据
    exitCode_ = atoi(codestr.c_str());
    result_ = atoi(reststr.c_str());
    return true;
#else
    //json
    Json::Value root;
    Json::Reader rd;
    rd.parse(in, root);
    exitCode_ = root["exitcode"].asInt();
    result_ = root["result"].asInt();
    return true;
#endif
}
  • debug:
void debug()
{
    std::cout << "#################################" << std::endl;
    std::cout << "exitCode_: " << exitCode_ << std::endl;
    std::cout << "result_: " << result_ << std::endl;
    std::cout << "#################################" << std::endl;
}

4. 服务端

我们继续用之前服务端代码,依旧是将任务派发给线程池,只需要修改Task队列中处理的任务,这就体现了之前做好封装的好处。

// 5.4 v3.3
Task t(serviceSock, peerIp, peerPort, netCal);
tp_->push(t);

服务端对收到的客户端发来的序列化数据,进行解码和反序列化,再交由处理calculator处理,计算出结果返回的是一个Response的对象,之后序列化并编码,将序列化数据再返回给用户端:

// 1. 全部手写 -- done
// 2. 部分采用别人的方案--序列化和反序列化的问题 -- xml,json,protobuf
void netCal(int sock, const std::string &clientIp, uint16_t clientPort)
{
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    // 9\r\n100 + 200\r\n    9\r\n112 / 200\r\n
    std::string inbuffer;
    while (true)
    {
        Request req;
        char buff[128];
        ssize_t s = read(sock, buff, sizeof(buff) - 1);
        if (s == 0)
        {
            logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort);
            break;
        }
        else if (s < 0)
        {
            logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s",
                       clientIp.c_str(), clientPort, errno, strerror(errno));
            break;
        }

        // read success
        buff[s] = 0;
        inbuffer += buff;
        std::cout << "inbuffer: " << inbuffer << std::endl;

        // 1. 检查inbuffer是不是已经具有了一个strPackage
        uint32_t packageLen = 0;
        std::string package = decode(inbuffer, &packageLen); 
        if (packageLen == 0) continue; // 无法提取一个完整的报文,继续努力读取吧
        std::cout << "package: " << package << std::endl;

        // 2. 已经获得一个完整的package
        if (req.deserialize(package))
        {
            req.debug();

            // 3. 处理逻辑, 输入的是一个req,得到一个resp
            Response resp = calculator(req); //resp是一个结构化的数据

            // 4. 对resp进行序列化
            std::string respPackage;
            resp.serialize(&respPackage);

            // 5. 对报文进行encode
            respPackage = encode(respPackage, respPackage.size());

            // 6. 将计算的结果序列化的返回给用户 -- 先简单的处理了以后再细说
            write(sock, respPackage.c_str(), respPackage.size());
        }
    }
}

计算函数:

static Response calculator(const Request &req)
{
    Response resp;
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;
    case '-':
        resp.result_ = req.x_ - req.y_;
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
        { // x_ / y_
            if (req.y_ == 0) resp.exitCode_ = -1; // -1. 除0
            else resp.result_ = req.x_ / req.y_;
        }
        break;
    case '%':
        { // x_ / y_
            if (req.y_ == 0) resp.exitCode_ = -2; // -2. 模0
            else resp.result_ = req.x_ % req.y_;
        }
        break;
    default:
        resp.exitCode_ = -3; // -3: 非法操作符
        break;
    }

    return resp;
} 

5. 客户端

客户端发将输入的内容进行判定是否符合输入规范,并将调用了makeReuquestRequest的对象进行初始化。对得到Request的对象进行序列化并编码,将序列化数据发送给服务端。待服务端处理完之后,再读回来Response对象的序列化数据,之后解码和反序列化,得到Response对象,最后将处理结果打印出来。

std::string message;
while (!quit)
{
    message.clear();
    std::cout << "请输入表达式>>> "; // 1 + 1 
    std::getline(std::cin, message); // 结尾不会有\n
    if (strcasecmp(message.c_str(), "quit") == 0)
    {
        quit = true;
        continue;
    }

    // 对用户输入的内容进行清洗
    // message = trimStr(message); // 1+1 1 +1 1+ 1 1+     1 1      +1 => 1+1 -- 就不处理了

    // 网络计算器计算请求:
    Request req;
    if(!makeReuquest(message, &req)) continue;
    req.debug();
    std::string package;
    req.serialize(&package); // done
    // std::cout << "debug->serialize-> " << package << std::endl;

    package = encode(package, package.size()); // done
    // std::cout << "debug->encode-> \n" << package << std::endl;

    ssize_t s = write(sock, package.c_str(), package.size());
    if (s > 0)
    {
        char buff[1024];
        size_t s = read(sock, buff, sizeof(buff)-1);
        if(s > 0) buff[s] = 0;
        std::string echoPackage = buff;
        Response resp;
        uint32_t len = 0;

        // std::cout << "debug->get response->\n" << echoPackage << std::endl;

        std::string tmp = decode(echoPackage, &len); // done
        if(len > 0)
        {
            echoPackage = tmp;
            // std::cout << "debug->decode-> " << echoPackage << std::endl;

            resp.deserialize(echoPackage);
            printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);
        }
    }
    else if (s <= 0)
    {
        break;
    }
}

makeReuquest函数:

bool makeReuquest(const std::string &str, Request *req)
{
    // 123+1  1*1 1/1
    char strtmp[BUFFER_SIZE];
    snprintf(strtmp, sizeof strtmp, "%s", str.c_str());

    char *left = strtok(strtmp, OPS);
    if (left == nullptr)
        return false;
    char *right = strtok(nullptr, OPS);
    if (right == nullptr)
        return false;

    char mid = str[strlen(left)];

    req->x_ = atoi(left);
    req->y_ = atoi(right);
    req->op_ = mid;
    std::cout << "req->x_: " << req->x_ << std::endl;
    return true;
}

6. 测试

我们将客户端输入之后被序列话之后的数据打印出来看看:

  • 自定协议版:

在这里插入图片描述

  • json版:

在这里插入图片描述

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yy_上上谦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值