【计算机网络】简学深悟启示录:序列化&&反序列化

文章目录

1.序列化&&反序列化

🤔什么是序列化反序列化?

序列化: 把程序内存中的数据结构 / 对象(比如 C++Json::ValuePython 的字典),转换成可存储 / 可传输的格式(比如 JSON 字符串、二进制流)的过程。
反序列化: 把存储 / 传输格式的数据,还原回程序可直接操作的内存数据结构 / 对象的过程。

😕为什么要序列化反序列化?

可以理解为定制了一个协议,对于网络来说需要以统一的格式进行传输,程序 A(比如你的服务端)要给程序 B(比如客户端)发送数据,内存中的对象无法直接通过网络传输,必须先序列化成通用格式(比如 JSON 字符串、Protobuf 二进制流),传输到对方后再反序列化为对方程序的内存对象

2.业务逻辑

序列化反序列化主要发生在应用层 ,虽然 C++ 有提供第三方的 Json 库,以及还有 Protobuf 可以使用,但是我们这里将通过自定义协议 的方式制定一个简单的网络计算器来理解序列化反序列的过程

这是简易的业务实现逻辑图,我们将依据这个进行说明

由于代码量较多,具体可查看Gitee仓库:https://gitee.com/zhang-zhanhua-000/linux/tree/master/Slzt&&Dlzt

3.具体剖析

这里我们将针对重点部分进行讲解,Socket.hpp 是对通用的创建套接字、绑定、设置监听、收发等功能进行封装,就不过多解释

3.1 公共协议:Protocol.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

// #define MySelf 1

const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";

std::string Encode(std::string &content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{
    std::size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);
    std::size_t len = std::stoi(len_str);
    // package = len_str + content_str + 2
    std::size_t total_len = len_str.size() + len + 2;
    if(package.size() < total_len) return false;

    *content = package.substr(pos+1, len);
    // earse 移除报文 package.erase(0, total_len);
    package.erase(0, total_len);

    return true;
}


// json, protobuf
class Request
{
public:
    Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper)
    {
    }
    Request()
    {}
public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // 构建报文的有效载荷
        // struct => string, "x op y"
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }
    bool Deserialize(const std::string &in) // "x op y"
    {
#ifdef MySelf
        std::size_t left = in.find(blank_space_sep);
        if (left == std::string::npos)
            return false;
        std::string part_x = in.substr(0, left);

        std::size_t right = in.rfind(blank_space_sep);
        if (right == std::string::npos)
            return false;
        std::string part_y = in.substr(right + 1);

        if (left + 2 != right)
            return false;
        op = in[left + 1];
        x = std::stoi(part_x);
        y = std::stoi(part_y);
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        x = root["x"].asInt();
        y = root["y"].asInt();
        op = root["op"].asInt();
        return true;
#endif
    }
    void DebugPrint()
    {
        std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl;
    }
public:
    // x op y
    int x;
    int y;
    char op; // + - * / %
};

class Response
{
public:
    Response(int res, int c) : result(res), code(c)
    {
    }

    Response()
    {}
public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // "result code"
        // 构建报文的有效载荷
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;
        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }
    bool Deserialize(const std::string &in) // "result code"
    {
#ifdef MySelf
        std::size_t pos = in.find(blank_space_sep);
        if (pos == std::string::npos)
            return false;
        std::string part_left = in.substr(0, pos);
        std::string part_right = in.substr(pos+1);

        result = std::stoi(part_left);
        code = std::stoi(part_right);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        result = root["result"].asInt();
        code = root["code"].asInt();
        return true;
#endif

    }
    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: "<< code << std::endl;
    }
public:
    int result;
    int code; // 0,可信,否则!0具体是几,表明对应的错误原因
};

这段代码由 #ifdef#else#endif,三者构成完整的条件编译分支,当 #define MySelf 1 定义时进入 #ifdef 分支,否则进入 #else#endif 结束条件编译分支,标志着条件编译逻辑的闭合

报文封包与解包

cpp 复制代码
std::string Encode(std::string &content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}
  • Encode:将传入的「业务有效内容」(如序列化后的请求 / 响应数据)封装为完整报文
cpp 复制代码
// "len""\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{
    std::size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);
    std::size_t len = std::stoi(len_str);
    // package = len_str + content_str + 2
    std::size_t total_len = len_str.size() + len + 2;
    if(package.size() < total_len) return false;

    *content = package.substr(pos+1, len);
    // earse 移除报文 package.erase(0, total_len);
    package.erase(0, total_len);

    return true;
}
  • Decode:核心是通过先解析长度再校验完整性最后提取内容 的逻辑实现完整报文提取,首先在传入的报文字符串 package 中查找第一个分隔符 \n,若找不到则说明报文连完整长度字段都没有,直接返回解析失败;找到后截取该分隔符之前的字符串作为长度字段 len_str,将其转换为整数 len(即业务内容的真实长度),接着计算完整报文的总长度 total_len(长度字段自身长度+业务内容长度+ 2\n 分隔符长度),若当前 package 的长度小于 total_len,说明报文不完整(业务内容未传输完毕),返回失败等待后续数据补充;若报文完整,则跳过第一个 \n,精准截取长度为 len 的业务内容并通过 content 指针输出,最后从 package 中删除已解析的完整报文段,避免粘包导致重复解析,返回解析成功,整个过程通过 长度预判 +完整性校验 确保只提取完整有效的业务内容

序列化反序列化分为 RequestResponse 两个类

  • 请求数据类 Request
    • 序列化方法 Serialize
cpp 复制代码
bool Serialize(std::string *out)
{
#ifdef MySelf
    // 构建报文的有效载荷:"x op y"
    std::string s = std::to_string(x);
    s += blank_space_sep;
    s += op;
    s += blank_space_sep;
    s += std::to_string(y);
    *out = s;
    return true;
#else
    Json::Value root;
    root["x"] = x;
    root["y"] = y;
    root["op"] = op;
    Json::StyledWriter w;
    *out = w.write(root);
    return true;
#endif
}

功能 :将 Request 对象中的成员变量(xyop)转换为字符串格式(业务有效内容),方便后续通过 Encode 封装为网络报文

两种实现方案(由 MySelf 宏控制)

  1. MySelf 宏定义时(自定义格式):拼接为 "x op y" 格式的字符串(如 "10 + 20"),使用空格分隔字段;
  2. MySelf 宏未定义时(Json 格式):创建 Json::Value 对象(root),将 xyop存入对应的 Json 字段;使用 Json::StyledWriterJson 对象转换为带格式化缩进的字符串;通过指针参数 out 输出序列化后的字符

返回值bool 类型,当前固定返回 true,预留了后续错误处理的扩展接口

  • 反序列化方法 Deserialize
cpp 复制代码
bool Deserialize(const std::string &in)
{
#ifdef MySelf
    std::size_t left = in.find(blank_space_sep);
    if (left == std::string::npos)
        return false;
    std::string part_x = in.substr(0, left);

    std::size_t right = in.rfind(blank_space_sep);
    if (right == std::string::npos)
        return false;
    std::string part_y = in.substr(right + 1);

    if (left + 2 != right)
        return false;
    op = in[left + 1];
    x = std::stoi(part_x);
    y = std::stoi(part_y);
    return true;
#else
    Json::Value root;
    Json::Reader r;
    r.parse(in, root);

    x = root["x"].asInt();
    y = root["y"].asInt();
    op = root["op"].asInt();
    return true;
#endif
}

功能 :将序列化后的字符串(in)转换为Request对象,提取出xyop并赋值给成员变量,用于服务器解析客户端发送的请求数据

两种实现方案(由 MySelf 宏控制)

  1. MySelf 宏定义时(自定义格式):
    查找第一个和最后一个空格的位置,分割出 part_x(第一个操作数字符串)和part_y(第二个操作数字符串);
    • 校验两个空格之间是否只有一个字符(运算符),若不符合则返回 false
    • part_xpart_y 转换为整数(std::stoi),赋值给xy,空格间的字符赋值给op
  2. MySelf 宏未定义时(Json 格式):创建 Json::Reader 对象(r),解析输入字符串inJson 对象 root;通过asInt()方法从 Json 对象中提取对应字段的值,赋值给 xyop

返回值bool 类型,自定义格式下会校验字段有效性,Json 格式下当前未处理解析失败的情况,预留扩展接口

  • 响应数据类 Response
    • 序列化方法 Serialize
cpp 复制代码
bool Serialize(std::string *out)
{
#ifdef MySelf
    // 构建报文的有效载荷:"result code"
    std::string s = std::to_string(result);
    s += blank_space_sep;
    s += std::to_string(code);
    *out = s;
    return true;
#else
    Json::Value root;
    root["result"] = result;
    root["code"] = code;
    Json::StyledWriter w;
    *out = w.write(root);
    return true;
#endif
}

功能 :将 Response 对象中的 resultcode 转换为字符串格式,方便后续封包传输

两种实现方案

  1. 自定义格式:拼接为 "result code" 格式(如 "30 0",表示计算结果 30,无错误)

  2. Json 格式:将 resultcode 存入 Json 对象,转换为格式化字符串输出

    • 反序列化方法 Deserialize
cpp 复制代码
bool Deserialize(const std::string &in)
{
#ifdef MySelf
    std::size_t pos = in.find(blank_space_sep);
    if (pos == std::string::npos)
        return false;
    std::string part_left = in.substr(0, pos);
    std::string part_right = in.substr(pos+1);

    result = std::stoi(part_left);
    code = std::stoi(part_right);

    return true;
#else
    Json::Value root;
    Json::Reader r;
    r.parse(in, root);

    result = root["result"].asInt();
    code = root["code"].asInt();
    return true;
#endif
}

功能 :将服务器返回的序列化字符串转换为 Response 对象,提取计算结果和错误码,供客户端解析

两种实现方案

  1. 自定义格式:查找空格分隔符,分割出结果字段和错误码字段,转换为整数赋值
  2. Json 格式:解析输入字符串为 Json 对象,提取对应字段的值赋值给成员变量

3.2 业务逻辑:Several.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include "Protocol.hpp"

enum
{
    Div_Zero = 1,
    Mod_Zero,
    Other_Oper
};

class ServerCal
{
public:
    ServerCal()
    {
    }

    Response CalculatorHelper(const Request &req)
    {
        Response resp(0, 0);
        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 '/':
        {
            if (req.y == 0)
                resp.code = Div_Zero;
            else
                resp.result = req.x / req.y;
        }
        break;
        case '%':
        {
            if (req.y == 0)
                resp.code = Mod_Zero;
            else
                resp.result = req.x % req.y;
        }
        break;
        default:
            resp.code = Other_Oper;
            break;
        }
        return resp;
    }

    std::string Calculator(std::string& package)
    {
        std::string content;
        bool r = Decode(package, &content);
        if(!r) return "";

        Request req;
        r = req.Deserialize(content);
        if(!r) return "";

        content = "";
        Response reps = CalculatorHelper(req);

        reps.Serialize(&content);
        content = Encode(content);

        return content;
    }

    ~ServerCal()
    {}
};

这是业务逻辑头文件,核心封装了ServerCal类,依赖协议层Protocol.hpp,完成客户端算术请求的完整处理:首先定义了Div_Zero(除数为0)、Mod_Zero(模数为0)、Other_Oper(无效运算符)三个异常错误码,类内提供两个核心方法,其中CalculatorHelper 作为内部辅助方法,接收结构化的Request请求对象,通过switch语句匹配+、-、*、/、%运算符执行对应算术运算,同时处理除法/取余的零值异常和无效运算符场景,封装运算结果与错误码到Response响应对象并返回;而Calculator作为对外暴露的唯一接口,衔接协议层与业务层,按「报文解包(Decode)→ 请求反序列化(Request::Deserialize)→ 执行算术运算(CalculatorHelper)→ 响应序列化(Response::Serialize)→ 报文封包(Encode)」的完整流程处理传入的网络报文,最终返回可直接用于网络传输的完整响应报文

3.3 Tcp服务封装:TcpServer.hpp

cpp 复制代码
#pragma once
#include <functional>
#include <string>
#include <signal.h>
#include "log.hpp"
#include "Socket.hpp"

using func_t = std::function<std::string(std::string &package)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback)
    {
    }
    bool InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        logger(Info, "init server .... done");
        return true;
    }
    void Start()
    {
        signal(SIGCHLD, SIG_IGN);
        signal(SIGPIPE, SIG_IGN);
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            if (sockfd < 0)
                continue;
            logger(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
            // 提供服务
            if (fork() == 0)
            {
                listensock_.Close();
                std::string inbuffer_stream;
                // 数据计算
                while (true)
                {
                    char buffer[1280];
                    ssize_t n = read(sockfd, buffer, sizeof(buffer));
                    if (n > 0)
                    {
                        buffer[n] = 0;
                        inbuffer_stream += buffer;

                        logger(Debug, "debug:\n%s", inbuffer_stream.c_str());

                        while (true)
                        {
                            std::string info = callback_(inbuffer_stream);
                            if (info.empty())
                                break;
                            logger(Debug, "debug, response:\n%s", info.c_str());
                            logger(Debug, "debug:\n%s", inbuffer_stream.c_str());
                            write(sockfd, info.c_str(), info.size());
                        }
                    }
                    else if (n == 0)
                        break;
                    else
                        break;
                }

                exit(0);
            }
            close(sockfd);
        }
    }
    ~TcpServer()
    {
    }

private:
    uint16_t port_;
    Sock listensock_;
    func_t callback_;
};

这里大部分都是包装好的方法直接使用封装好的,重点说下:func_t 是通过 C++11 中的 using 关键字定义的 std::function 函数对象类型别名,具体定义为 using func_t = std::function<std::string(std::string &package)>,它可以接收符合该签名的任意可调用对象(如 ServerCal::Calculator 方法),使得开发者无需修改 TcpServer 的网络层代码,只需替换不同的业务回调函数,即可实现不同功能的 TCP 服务器,极大提升了代码的可复用性和扩展性

4.服务端启动

ServerCal.cpp:

cpp 复制代码
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include <unistd.h>
// #include "Daemon.hpp"

static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl; 
}

// ./servercal 8080
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    ServerCal cal;
    TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tsvp->InitServer();
    // Daemon();
    daemon(0, 0);
    tsvp->Start();

    return 0;
}

由于前面完整封装好了大部分功能,所以启动直接调用就好了,但是这里我想重点强调一下 bind,不用 bind 的话就得在 callback 处直接创建对象来调用此方法,但是这样如果逻辑修改就要多个文件修改,很麻烦,保证无需修改 TcpServer 的网络层代码,所以用包装类+

🤔直接传成员对象的函数 ServerCal::Calculator 不行吗,为什么要用 bind

  1. 语法层面: 类的成员函数无法直接转换为普通函数指针 std::function
    普通函数(全局函数、静态成员函数)的地址是一个「独立的内存地址」,可以直接赋值给 std::function 或函数指针;而 非静态成员函数 的原型中,隐含了一个「隐藏参数 this 指针」(编译器自动添加),用于指向调用该成员函数的对象实例

简单说:

  • 普通函数签名:std::string func(std::string &package)(无隐藏参数)
  • 成员函数签名:std::string ServerCal::Calculator(ServerCal *this, std::string &package)(隐含this指针)

TcpServer 期望接收的是「符合 std::string (std::string &) 签名的可调用对象」,而直接传递 &ServerCal::Calculator,其签名与期望不匹配,编译器会直接报错(语法不合法)

  1. 逻辑层面: 缺少对象上下文,成员函数无法执行
    成员函数是「属于类的对象实例」的,脱离了具体对象,成员函数无法访问类的非静态成员变量/方法(因为没有 this 指针指向具体对象)

即使语法允许传递成员函数,直接传递 &ServerCal::Calculator 也只是传递了"函数的逻辑",没有传递"该函数要作用的对象(cal)",服务器调用该函数时,会因为缺少具体对象而无法执行

5.客户端启动

cpp 复制代码
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"

static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./clientcal ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    Sock sockfd;
    sockfd.Socket();
    bool r = sockfd.Connect(serverip, serverport);
    if(!r) return 1;

    srand(time(nullptr) ^ getpid());
    int cnt = 1;
    const std::string opers = "+-*/%=-=&^";

    std::string inbuffer_stream;
    while(cnt <= 10)
    {
        std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
        int x = rand() % 100 + 1;
        usleep(1234);
        int y = rand() % 100;
        usleep(4321);
        char oper = opers[rand()%opers.size()];
        Request req(x, y, oper);
        req.DebugPrint();

        std::string package;
        req.Serialize(&package);

        package = Encode(package);

        write(sockfd.Fd(), package.c_str(), package.size());
        // std::cout << "这是最新的发出去的请求: " << n << "\n" << package;
        // n = write(sockfd.Fd(), package.c_str(), package.size());
        // std::cout << "这是最新的发出去的请求: \n" << n << "\n" << package;
        // n = write(sockfd.Fd(), package.c_str(), package.size());
        // std::cout << "这是最新的发出去的请求: \n" << n << "\n" << package;
        // n = write(sockfd.Fd(), package.c_str(), package.size());
        // std::cout << "这是最新的发出去的请求: \n" << n << "\n" << package;


        char buffer[4096];
        ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 我们也无法保证我们能读到一个完整的报文
        if(n > 0)
        {
            buffer[n] = 0;
            inbuffer_stream += buffer; // "len"\n"result code"\n
            std::cout << inbuffer_stream << std::endl;
            std::string content;
            bool r = Decode(inbuffer_stream, &content); // "result code"
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);

            resp.DebugPrint();
        }

        std::cout << "=================================================" << std::endl;
        sleep(1);

        cnt++;
    }


    sockfd.Close();
    return 0;
}

这里就是很简单的测试代码,向指定 TCP 服务器连续发送 10 次随机计算请求(包含随机操作数、随机运算符),并接收服务器返回的计算响应结果进行解析和打印,整体流程贴合网络通信的「序列化→编码→发送→接收→解码→反序列化」规范

6.效果展示


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

相关推荐
2301_810970392 小时前
第一次渗透作业
web安全·php
ID_180079054732 小时前
乐天(Letian)商品详情API接口的调用示例与代码实现
开发语言·python
一位搞嵌入式的 genius2 小时前
深入理解 JavaScript 原型与继承:从基础到进阶
开发语言·前端·javascript
晨非辰2 小时前
C++波澜壮阔40年|类和对象篇:拷贝构造与赋值重载的演进与实现
运维·开发语言·c++·人工智能·后端·python·深度学习
m0_719084112 小时前
滴滴滴滴滴
java·开发语言
董世昌412 小时前
深度解析var、let、const的区别与最佳使用场景
开发语言·前端·javascript
FJW0208142 小时前
Python中的闭包
开发语言·python
C_心欲无痕2 小时前
Next.js 平行路由:构建模块化动态布局
开发语言·前端·javascript
100编程朱老师2 小时前
fping命令详解
开发语言·php