序列化和反序列化

背景

TCP 协议作为一种面向连接的传输层协议,其核心特性之一是 "面向字节流" 的本质。这意味着数据在传输过程中被视为连续的字节序列,协议本身并不为数据划分天然的边界,仅能保证数据按序到达,但无法确保接收方读取的数据与发送方单次发送的报文完全对应,从而会带来数据接收的不确定性。

这种特性直接导致了 "粘包" 现象的产生。所谓粘包,是指在基于流的传输协议(如 TCP)中,发送方多次发送的小数据包,在被接收方接收时可能 "粘" 在一起,形成一个或多个不完整或合并的数据包。例如发送方分两次发送 "Hello" 和 "World",接收方可能一次收到 "HelloWorld",也可能收到 "Hel""loWorld" 这类拆分或合并后的结果。

粘包现象的核心原因在于 TCP 作为流协议,不维护 "数据包边界" 。协议会根据自身缓冲区的实际情况,对数据进行合并或拆分后再发送,这就使得接收方无法直接将读取到的数据与发送方的单次发送操作对应起来,需要额外的机制来解决数据的正确解析问题。

1.TCP 发送方的数据处理逻辑

  • TCP是传输控制协议,write/send接口仅仅是负责将数据拷贝到发送缓冲区,不负责将数据进行网络传输。
  • TCP是操作系统的网路模块,所以把数据交给TCP 就是把数据交给操作系统

2.TCP 接收方的数据处理逻辑

  • 应用层通过read接口读取数据时,该接口仅完成数据从操作系统 TCP 接收缓冲区到用户层缓冲区的拷贝
  • 读取的数据量不确定:可能是发送方一次发送数据的一部分、多段数据的拼接,或恰好一段完整数据,TCP 不提供 "报文完整性" 标识。

正因为 TCP 仅传输字节流、不保证接收数据的 "报文完整性",应用层必须通过协议,才能确保数据在传输后被正确识别和处理。

协议是一种"约定",socket api的接口, 在读写数据时, 都是按"字符串" 的方式来发送接收的。如果我们要传输一些"结构化的数据"该怎么办呢?

1.案例:网络版计算器

在网络服务(如网络计算器)中,需通过 "应用层协议" 约定数据格式,确保发送方数据能被接收方正确解析,常见两种基础方案及问题如下:

1.1 方案一:固定格式字符串

流程:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数都是整形;
  • 两个数字之间会有一个字符是运算符,运算符只能是 + ;
  • 数字和运算符之间没有空格;

问题: 扩展新运算符/操作数需重构格式,特殊字符易冲突,且仅适配简单数据类型。

1.2 方案二:结构体序列化 / 反序列化

流程:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 "序列化"

**问题:**因跨平台不兼容(端序、内存对齐、类型大小差异)和版本适配差,被应用层禁用。

1.3 核心结论

无论何种方案,只要保证 "发送方构造的数据能被接收方正确解析",即为有效的应用层协议,而序列化与反序列化是实现该目标的关键技术。

2.序列化与反序列化的定义及流程

在网络消息传输场景中,消息并非直接逐条发送,需通过序列化与反序列化处理,确保结构化数据能跨网络正确传递与识别。

2.1 核心概念

  • 序列化:发送方将结构化数据(如含 "消息内容、发送者昵称、发送时间" 的结构体),按约定规则转为可网络传输的字符串的过程。
  • 反序列化:接收方按与发送方一致的规则,将网络接收的字符串还原为原结构化数据(打散还原为结构体)的过程。

2.2 完整流程(以网络消息传输为例)

  1. 发送方序列化处理:发送方先将消息的结构化信息(如"消息内容(如'你好')、发送者昵称、发送时间")封装成一个结构体,再按照约定规则将该结构体转换为可通过网络传输的字符串,这个将结构化数据转为传输格式的过程即为序列化。
  2. 数据传输与转发:序列化后的字符串会被发送至服务器,由服务器接收并转发给目标接收方,完成数据的中间传递。
  3. 接收方反序列化处理:接收方收到服务器转发的字符串后,需遵循与发送方完全一致的规则,将字符串重新还原为包含"消息内容、昵称、时间"的结构化数据(即打散还原为原结构体),这个从传输格式还原为结构化数据的过程即为反序列化。

通过序列化与反序列化的配合,能保证发送方和接收方对同一份结构化数据的识别逻辑一致,最终实现网络消息的高效、准确收发。

3.常用序列化工具:JSON(轻量级数据交换格式)

3.1 JSON 的优势与适用场景

  • 优势:支持可视化的序列化 / 反序列化,方便调试;轻量级,跨语言支持(Java、C、C++ 等均兼容)。
  • 对比:与 protobuf 相比,JSON 侧重调试便捷性,protobuf 侧重传输效率。

3.2 C++ 环境下 JSON 的安装与使用

(1)安装步骤

通过命令安装第三方库jsoncpp:

bash 复制代码
sudo yum install -y jsoncpp-devel

安装后会自动提供头文件和库文件:

我们主要会使用到json.h头文件。

(2)使用注意事项

  • **头文件包含:**系统默认搜索/usr/include目录,而json.h位于/usr/include/jsoncpp/json/下,需按如下方式包含:

    cpp 复制代码
    #include <jsoncpp/json/json.h>
  • **编译链接:**编译 C++ 代码时,需在命令末尾加-ljsoncpp选项(JsonCpp 为第三方库,不在编译器默认搜索范围),否则会报 "undefined reference to Json::Value" 等链接错误。

(3)JSON 的核心格式

以key:value形式组织数据,是结构化数据传输字符串之间的桥梁。

3.3 代码示例

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>  // 包含Jsoncpp库的头文件
using namespace std;

int main()
{
    // 构建Json对象root,作为整个JSON数据的根节点
    Json::Value root;
    
    // 向root对象中添加基本类型的键值对
    root["name"] = "lynn";       // 添加字符串类型
    root["age"] = 23;            // 添加整数类型
    root["is_worked"] = false;   // 添加布尔类型

    // 构建被root嵌套的对象address,用于存储地址信息
    Json::Value address;
    address["city"] = "Shenzhen";  // 城市信息
    
    // 直接在address对象中嵌套detail对象,并添加street字段
    // 无需单独定义detail对象,可以直接通过键名层级访问
    address["detail"]["street"] = "华尔街"; 
    
    // 将address对象作为值添加到root对象中,形成嵌套结构
    root["address"] = address;

    // 构建一个JSON数组hobbies,用于存储爱好信息
    // 初始化时指定为arrayValue类型
    Json::Value hobbies(Json::arrayValue);
    hobbies.append("tennis");      // 向数组添加元素
    hobbies.append("sax");
    hobbies.append("basketball");
    root["hobbies"] = hobbies;     // 将数组添加到root对象

    // 序列化操作:将JSON对象转换为字符串
    // StyledWriter会生成格式化的、带缩进的JSON字符串,便于人类阅读
    cout << "===StyledWriter输出===" << endl;
    Json::StyledWriter sw;
    cout << sw.write(root) << endl;  // 输出格式化的JSON

    // FastWriter会生成紧凑的JSON字符串,没有多余的空格和换行,适合网络传输
    cout << "===FastWriter输出===" << endl;
    Json::FastWriter fw;
    cout << fw.write(root) << endl;  // 输出紧凑的JSON

    // 反序列化操作:将JSON字符串解析为JSON对象
    Json::Value parsedRoot;  // 用于存储解析后的JSON数据
    Json::Reader r;          // 声明JSON解析器对象
    
    // 将FastWriter生成的JSON字符串解析到parsedRoot对象中
    // 第一个参数是JSON字符串,第二个参数是解析后存储的对象
    r.parse(fw.write(root), parsedRoot);

    // 从解析后的JSON对象中读取数据并输出
    cout << "===读取数据===" << endl;
    // 使用asString()等方法将JSON值转换为对应的数据类型
    cout << "name: " << parsedRoot["name"].asString() << endl;
    cout << "age: " << parsedRoot["age"].asInt() << endl;  
    cout << "is_worked: " << parsedRoot["is_worked"].asString() << endl;  
    cout << "city: " << parsedRoot["address"]["city"].asString() << endl;
    cout << "detail street: " << parsedRoot["address"]["detail"]["street"].asString() << endl;
    
    // 读取数组中的元素
    cout << "hobbies: ";
    // 循环遍历数组,size()方法获取数组长度
    for (int i = 0; i < parsedRoot["hobbies"].size(); ++i)
    {
        cout << parsedRoot["hobbies"][i].asString() << " ";
    }
    cout << endl;

    return 0;
}

3.4 Json在网络协议中的应用

cpp 复制代码
#ifndef PROTOCAL_HPP // 若未定义过 PROTOCAL_HPP
#define PROTOCAL_HPP // 定义 PROTOCAL_HPP

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
// #define MySelf 0
using namespace std;

const string blank_space_sep = " ";
const string protocal_sep = "\n";

// 为有效载荷添加报头:"len""\n""x op y""\n"
string encode(string &content)
{
    string package = to_string(content.size());
    package += protocal_sep;
    package += content;
    package += protocal_sep;
    return package;
}

// 从报文中解析出有效载荷
bool decode(string &package, string *content)
{
    size_t pos = package.find(protocal_sep);
    if (pos == string::npos)
        return false;
    string len_str = package.substr(0, pos);
    size_t len = stoi(len_str);
    size_t total_len = len_str.size() + len + 2;
    if (total_len > package.size())
        return false;
    *content = package.substr(pos + 1, len);
    package.erase(0, total_len); // 为了处理下一条报文,content提取到一条完整的报文后就将它从package移除

    return true;
}

// 封装「客户端请求」数据
class Request
{
public:
    Request(int data1, int data2, char oper)
        : x(data1), y(data2), op(oper)
    {
    }

    Request() {}

public:
    // 序列化 ------ 对象转字符串
    // 将 Request 对象(包含 x、op、y)序列化为字符串 "x op y" 格式
    bool serialize(string *out)
    {
#ifdef MySelf
        // 一条报文的格式:"len""\n""x op y"

        // 构建报文的有效载荷
        //  struct => string, "x op y"
        string s = to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += 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); // 用紧凑格式生成JSON字符串
        return true;
#endif
    }

    // 反序列化 ------ 字符串转对象
    // 从字符串解析出数据并赋值给 Request 对象(x、y、op)
    bool deserialize(const string &in)
    {
#ifdef MySelf
        size_t left = in.find(blank_space_sep);
        if (left == string::npos)
            return false;
        string part_x = in.substr(0, left);

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

        if (left + 2 != right)
            return false;
        op = in[left + 1];
        x = stoi(part_x);
        y = 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()
    {
        cout << "新请求构建完成:" << x << op << y << "=?" << endl;
    }

public:
    int x;
    int y;
    char op; // + - * / %
};

// 封装「服务器响应」数据
class Response
{
public:
    Response(int res, int code)
        : _result(res), _code(0)
    {
    }

    Response() {}

public:
    bool serialize(string *out)
    {
#ifdef MySelf
        // 一条报文的格式:"len""\n""result code"

        // 构建报文的有效载荷
        string s = to_string(_result);
        s += blank_space_sep;
        s += 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); // 用紧凑格式生成JSON字符串
        return true;
#endif
    }

    bool deserialize(const string &in)
    {
#ifdef MySelf
        size_t pos = in.find(blank_space_sep);
        if (pos == string::npos)
            return false;
        string part_res = in.substr(0, pos);
        string part_code = in.substr(pos + 1);

        _result = stoi(part_res);
        _code = stoi(part_code);

        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()
    {
        cout << "结果响应完成, result:" << _result << " code:" << _code << endl;
    }

public:
    int _result;
    int _code; // 0-可信 !0-不可信,具体是积,表明错误原因
};

#endif // 结束保护,若已定义过 PROTOCAL_HPP,则跳过中间内容

3.5 应用场景

以科大讯飞的语音识别功能为例,其技术常被集成到耳机、语音笔等各类产品中,实现中文与英文互译、语音内容识别等功能。

这类产品的工作原理是:设备本身具备网络连接能力,会先将用户输入的语音数据上传至科大讯飞的云端服务器;云端服务器完成语音识别与解析后,会以JSON格式将处理结果返回给设备;设备再通过反序列化提取JSON数据中的有效信息,最终将识别或翻译结果呈现给用户。

在日常开发网络服务时,开发者通常无需自行编写网络协议,尤其是应用层协议。这是因为主流的应用层协议(如HTTP、SMTP等)已由行业标准化并预先定义好,可直接用于数据传输、邮件发送等场景;同时,也有现成的网络功能库(例如HTTP库)将复杂的网络通信逻辑封装起来,开发者无需关注底层实现,只需调用库中提供的方法,就能快速实现所需的网络功能,大幅降低了开发难度与成本。

但以下情况需自行写代码:

  • 现有协议(如HTTP、SMTP)无法满足特殊需求,比如要设计私有通信规则、定制专属数据格式。
  • 现有网络库无法覆盖功能,例如需深度优化传输效率、适配特殊硬件接口,或实现自定义加密、流量控制等逻辑。

4.重谈OSI七层模型

  • 会话层相当于代码层的TCPserver,由它获取新连接,它负责维护整个连接的在线情况。
  • 表示层就相当于代码中的协议,也就是固有的数据格式,网络序列与主机序列的转换、序列和反序列化都在这一层完成。
  • 应用层在代码中就是处理数据的部分,也就是计算部分。
相关推荐
郝学胜-神的一滴3 小时前
策略模式:模拟八路军的抗日策略
开发语言·c++·程序人生·设计模式·策略模式
tuokuac3 小时前
如何确定虚拟机的IP
网络·网络协议·tcp/ip
青草地溪水旁3 小时前
linux修改权限命令chmod
linux·chmod
不午睡的探索者4 小时前
音视频开发入门:FFmpeg vs GStreamer
c++·github·音视频开发
楼田莉子5 小时前
C++算法学习专题:前缀和
c++·学习·算法·leetcode·蓝桥杯
Jooolin5 小时前
【C++】C++11出来之后,到目前为止官方都做了些什么更新?
数据结构·c++·ai编程
羑悻的小杀马特5 小时前
从Cgroups精准调控到LXC容器全流程操作:用pidstat/stress测试Cgroups限流,手把手玩转Ubuntu LXC容器全流程
linux·服务器·数据库·docker·lxc·cgroups
长沙红胖子Qt5 小时前
VTK开发笔记(三):熟悉VTK开发流程,编写球体,多半透明球体Demo
c++·qt
Jooolin5 小时前
【C++】C++11都有什么新特性?
c++·ai编程·编程语言