首页 > 系统相关 >Linux网络:序列化与反序列化

Linux网络:序列化与反序列化

时间:2024-10-21 12:45:49浏览次数:12  
标签:const 网络 Value Json 数组 Linux 序列化 root

Linux网络:序列化与反序列化


序列化与反序列化

在网络通信中,最重要的就是通过接口,将数据通过网络发送给另一台主机。那么另一台主机收到数据后,就可以对数据进行处理了。

但是网络通信中,数据不是简单的将所有的数据堆砌在一起,然后一起发送出去。相反的,数据应该以特定的形式组织起来,才能让接收方进行合理的解析。

比如以下数据:

你好2024.10.21cool boy

这是一个叫做cool boy的用户,在2024.10.21发送了一条"你好"的消息。对于人来说,这是很好理解的,但是对于计算机来说,这就很难处理了。比如说它也可以被解析为用户名是"你好"的用户发送了一条叫做cool boy的消息。用户名、时间、消息内容,这三个字段的分界符在哪里?如何区分这三个区域?

对于UDP来说,一个消息放在一个报文中一起发送。但是对于TCP来说,它是面向字节流的,那么就可以能出现多个消息的粘包问题:

你好2024.10.21cool boy我是一个程序员2024.10.21cool boy

比如收到这样一条TCP字节流消息,计算机又要如何区分两个报文的边界?

你会发现,简单的把数据堆在一起,这个数据就会难以解析。因此要把数据按照一定的规律合理组织在一起,并且约定好各个字段的含义,如果是TCP通信,还需要约定好一条消息的定界符,避免粘包问题。这种行为称为序列化。而从组织好的数据中还原出目标信息,称为反序列化

当前流行的序列化协议:

  • JSON
  • XML
  • 二进制格式

其中json全称JavaScript Object Notation,即JavaScript 对象 表示法,顾名思义它是由JavaScript提出的一种序列化规范,这是一种文本形式的协议,以字符串的形式存储与解析数据。

比如刚才的消息可以表示为:

{
	"message": "你好",
	"time": "2024.10.21",
	"name": "cool boy"
}

这样整个消息就被分为了三个部分,并且每个字段都以键值对的形式表示。这样就可以很明确的表示"你好"是一条消息,而cool boy是用户名。而整条报文的最外层,有一个{},这就可以起到TCP的定界符的作用。

除去json还有很多数据序列化格式,比如二进制格式可以使用protobuf等。接下来博客讲解json的序列化格式,以及它的C++接口。


json

json可以保存标量类型,数组,对象三种形式的数据,规则如下:

  1. 数据以键值对key: value的形式存储
  2. 多条数据使用,隔开
  3. key的类型必须是字符串
  4. value的类型可以是标量类型,数组,对象
  5. 数组用[]表示
  6. 对象用{}表示

对于整个json消息,它可以是数组或对象。

数组形式:

[
    "hello",
    "world",
    true,
    3.14
]

对象形式:

{
	"name": "cool boy",
	"age": 12,
	"adult": false
}

首先,数组内部的数据,不需要以键值对的形式存储,而数组最外侧使用[]表示。在数组中,可以存储任何形式的数据,比如true是布尔类型,3.14是浮点型。

而在对象形式中,每个字段都要以key:value的形式存储,最外侧使用{},对象中也可以存储任意标量类型。

另外的,数组与对象之间,还可以进行嵌套:

[
    {
        "name": "张三",
        "age": 12,
        "adult": false
    },
    {
        "name": "李四",
        "age": 20,
        "adult": true
    }
]

比如这是一个数组,内部有两个对象,这就是数组内嵌套对象。

{
    "name": "张三",
    "age": 12,
    "friend": [
        "李四",
        "王五",
        "赵六"
    ]
}

这是一个对象,在对象中,friend存储的是一个数组,数组内又存储了多个字符串。

想要处理json数据,还是比较麻烦的,比如说要考虑括号匹配问题,数据类型的转化问题。但是不用担心,目前的主流语言,都要专门支持json格式的库,其中C++可以使用jsoncpp库完成数据的序列化与反序列化。


jsoncpp

Linux中,下载jsoncpp非常简单,以Ubuntu为例:

sudo apt install libjsoncpp-dev

执行完指令,就可以使用该库了,只需包含头文件:

#include <jsoncpp/json/json.h>

Value对象

jsoncpp的所有操作,都基于Json::Value类,该类用于保存一个json格式的对象。

基于C++的操作符重载特性,Json::Value类实现了operator[],完成对元素的增删改,函数声明如下:

Value& operator[](const char* key);
Value& operator[](const String& key);

这两个接口,是对json对象的操作,[]内放key值,而key必须是一个字符串,此处支持char*std::string两者格式的字符串。

std::map用法一致,如果[]内的值存在,那么就返回该值,如果不存在,就添加该值:

Json::Value root;
root["name"] = "张三";
root["age"] = 12;

std::cout << root["age"] << std::endl;

以上代码中,创建了一个json对象root,并设置了“name”"age"属性,最后又获取并输出"gae"

如果Json::Value存储的是一个json数组,那么使用以下两个接口:

Value& operator[](ArrayIndex index);
Value& operator[](int index);

这两个接口根据下标index返回元素的引用,下标从0开始。

如果存储了json数组,那么就不能通过operator[key] = value的形式插入值了,因为数组不需要key值,此时使用以下接口:

void append(const Json::Value& value);

只需要append(value)即可在数组尾插一个元素。

Json::Value root;
root.append("张三");
root.append("李四");
root.append("王五");
root.append("赵六");

std::cout << root[2] << std::endl;

以上代码,往数组中插入了四个值,并输出下标为2的元素。

  • 类型转化

如果仔细观察,你会发现上面所有接口,操作的都是Json::Value,比如append(const Json::Value& value)

其实Json::Value可以转化为绝大部分的标量类型:

Json::Value& operator=(bool value);
Json::Value& operator=(int value);
Json::Value& operator=(unsigned int value);
Json::Value& operator=(Int64 value);
Json::Value& operator=(UInt64 value);
Json::Value& operator=(double value);
Json::Value& operator=(const char* value);
Json::Value& operator=(const std::string value);

也就是说之前的所有接口,其实Json::Value这个参数可以表达所有的标量类型,因此不论是数组的appent还是对象的operator[],都可以插入任意标量类型的数据,这些标量数据最后会转化为Json::Value

  • 数组和对象操作

再补充一些数组和对象的通用接口:

size_t size();
bool empty();
void clear();
  1. size:返回数组或对象中元素数量
  2. empty:检查数组或对象是否为空
  3. clear:清空数组或对象的所有元素

序列化反序列化

了解如何往Json::Value中填写数据后,接下来就要考虑如何进行序列化与反序列化了。

Writer

jsoncpp通过Json::Writer类进行序列化操作,但是该类是一个抽象类,无法实例化出对象。它有两个派生类StyledWriterFastWriter,这两个类的对象完成具体的序列化操作。

Json::Writer类中,有一个writer函数,声明如下:

virtual String write(const Value& root) = 0;

这是最核心的序列化函数,= 0表示这个函数由子类重写,也就是StyledWriterFastWriter对这两个函数进行了重写。

函数输入一个Json::Value对象,随后会把这个对象进行序列化,将序列化好的数据作为返回值。

构造并序列化以下json

[
    "hello",
    "world",
    12,
    false,
    [
        "abc",
        "def"
    ],
    {
        "gender": "boy",
        "age": 12
    }
]

这是一个数组,内部包含了六个元素,其中第五个元素是另一个数组,第六个元素是一个对象。

构造对象:

Json::Value root;
root.append("hello");
root.append("world");
root.append(12);
root.append(false);

Json::Value arr;
arr.append("abc");
arr.append("def");
root.append(arr);

Json::Value obj;
obj["gender"] = "boy";
obj["age"] = 12;
root.append(obj);

对于前四个标量数据,直接通过append添加到root结尾即可。而对于嵌套的数组和对象,要创建另一个Json::Value,先进行数据填充,在添加到root结尾。

序列化:

Json::StyledWriter swriter;
std::string style = swriter.write(root);
std::cout << "StyledWriter:" << std::endl;
std::cout << style << std::endl;

Json::FastWriter fwriter;
std::string fast = fwriter.write(root);
std::cout << "FastWriter:" << std::endl;
std::cout << fast << std::endl;

此处的序列化分别实验了StyledWriter FastWriter,它们都可以完成序列化。通过write(root)接口完成序列化后,将序列化的结果输出,结果:

StyledWriter:
[
   "hello",
   "world",
   12,
   false,
   [ "abc", "def" ],
   {
      "age" : 12,
      "gender" : "boy"
   }
]

FastWriter:
["hello","world",12,false,["abc","def"],{"age":12,"gender":"boy"}]

可以看出,其实两种序列化形式的数据内容是一样的,区别在于格式:

  1. StyledWriter:会给json数据添加换行格式,让数据更加可视化
  2. FastWriter:去除所有的换行格式,整个json就是一行数据

在实际中,选用哪一种都可以,两者区别其实不大。

现在将以上对象序列化到test.json文件中:

Json::StyledWriter swriter;
std::string str = swriter.write(root);
std::ofstream ofs("test.json");
ofs << str;
ofs.close();

使用std::string接收到write返回值后,直接通过文件流对象输出到文件即可。


Reader

当需要反序列化数据时,就要用到Json::Reader类,核心反序列化函数如下:

bool parse(const std::string& document, Value& root,
           bool collectComments = true);
bool parse(IStream& is, Value& root, bool collectComments = true);

Writer不同,Reader可以直接使用,无需使用它的派生类。不论是通过StyledWriter 还是FastWriter序列化的数据,Reader都可以直接反序列化。

第一个函数接收一个std::string和一个Json::Value root对象的引用,parse直接反序列化字符串内的数据给root对象,第三个参数不用管,默认为true即可。

第二个函数则是通过文件流IStream& is读取数据,然后反序列化到root中。

当拿到一个未知的Json::Value,又要如何解析?这就要用到isas系列的接口:

bool isNull() const; // 检查值是否为为空
bool isBool() const; // 检查是否为布尔类型
bool isInt() const;  // 检查是否为整型
bool isInt64() const; // 检查是否为64位整型
bool isUInt() const; // 检查是否为无符号整型
bool isUInt64() const; // 检查是否为64位无符号整型
bool isIntegral() const; // 检查是否为整数或者可转化为整数的浮点数
bool isDouble() const; // 检查是否为浮点数
bool isNumeric() const; // 检查是否为数字,整数或浮点数
bool isString() const; // 检查是否为字符串
bool isArray() const; // 检查是否为数组
bool isObject() const; // 检查是否为对象

由于拿到Json::Value后,无法确定内部的值是对象,数组,还是一些标量。因此提供了以上is系列接口,来检测数据类型,防止出现接收数据时,类型不匹配导致的错误。

const char* asCString() const;
String asString() const;
Int asInt() const;
UInt asUInt() const;
Int64 asInt64() const;
UInt64 asUInt64() const;
float asFloat() const;
double asDouble() const;
bool asBool() const;

当确定了类型后,就可以通过以上的as系列接口,直接把Json::Value转化为对应的标量类型,进行后续操作。

而对于数组可能不知道它有多少个元素,可以通过size接口获取下标范围。而对于对象,可能不知道他有哪些key,此时可以通过以下接口:

Members getMemberNames() const;

如果Json::Value内部存储的是对象,该函数用于获取一个Json::Value的所有key,并存储在Json::Value::Members中,Json::Value::Members是一个数组,可以直接通过下标访问。

对象常用以下格式解析:

Json::Value::Members keys = root.getMemberNames();
for (int j = 0; j < keys.size(); j++)
{
	Json::Value item = root[keys[j]]
	// 解析item
}

确定root是一个对象后,先root.getMemberNames()拿到所有的key。随后通过一个循环遍历所有的keyroot[keys[j]]就是对象root的一个成员,随后在对该对象做解析操作即可。

由于不论是标量类型,还是数组或者对象,都是使用Json::Value表示的,所以常用递归的形式来解析:

void print(Json::Value root)
{
    if (root.isInt())
        std::cout << root.asInt() << std::endl;
    else if (root.isBool())
        std::cout << root.asBool() << std::endl;
    else if (root.isString())
        std::cout << root.asString() << std::endl;
    else if (root.isArray())
    {
        std::cout << "[" << std::endl;
        for (int i = 0; i < root.size(); i++)
            print(root[i]);
        std::cout << "]" << std::endl;
    }
    else if (root.isObject())
    {
        std::cout << "{" << std::endl;
        Json::Value::Members keys = root.getMemberNames();
        for (int j = 0; j < keys.size(); j++)
        {
            std::cout << keys[j] << ": ";
            print(root[keys[j]]);
        }
        std::cout << "}" << std::endl;
    }
}

当该函数接收到一个Json::Value root后,先判断他是不是基本的标量类型,如果是标量类型,那么直接输出即可。

如果是数组,那么for (int i = 0; i < root.size(); i++)遍历它的所有下标,每个元素root[i]有可能是标量,嵌套数组,嵌套对象。但是没关系,这些类型都被统一为了Json::Value,直接递归print(root[i])

对象同理,先通过 root.getMemberNames()拿到所有的key,所有root[keys[j]]就是对象内的元素,这个元素同样有可能是标量,嵌套数组,嵌套对象,它们被统一为了Json::Value,直接递归print(root[keys[j]])

以递归形式读取刚才的test.json

void print(Json::Value root)
{
    if (root.isInt())
        std::cout << root.asInt() << std::endl;
    else if (root.isBool())
        std::cout << root.asBool() << std::endl;
    else if (root.isString())
        std::cout << root.asString() << std::endl;
    else if (root.isArray())
    {
        std::cout << "[" << std::endl;
        for (int i = 0; i < root.size(); i++)
            print(root[i]);
        std::cout << "]" << std::endl;
    }
    else if (root.isObject())
    {
        std::cout << "{" << std::endl;
        Json::Value::Members keys = root.getMemberNames();
        for (int j = 0; j < keys.size(); j++)
        {
            std::cout << keys[j] << ": ";
            print(root[keys[j]]);
        }
        std::cout << "}" << std::endl;
    }
}

int main()
{
    std::ifstream ifs("test.json");
    Json::Reader re;
    Json::Value root;
    re.parse(ifs, root);
    print(root);
    
	return 0;
}

main函数中,打开文件流ifs("test.json"),随后re.parse(ifs, root)直接从文件流中读取数据,解析到root对象中,最后通过print函数递归输出该数据。

输出结果:

[
hello
world
12
0
[
abc
def
]
{
age: 12
gender: boy
}
]

这个结果没有控制缩进,所有格式有点丑。


标签:const,网络,Value,Json,数组,Linux,序列化,root
From: https://blog.csdn.net/fsdfafsdsd/article/details/143099867

相关文章

  • linux基本命令培训
    Linux基本命令切换目录:cd显示当前所在目录:pwd新建目录:mkdir新建文件:touch vi删除:rm  (rm-rfaa1.txt)查看档案和目录:ls   ll   ls-l复制:cp移动:mv查看文件:cat (cat1.txt,   cat1.txt2.txt>3.txt)一页一页的显示文件内容:more、less(more1.txt-àctrl+f......
  • 机器学习与神经网络的未来展望
      一、引言  近日,一个历史性的时刻在科学界引起了轰动:2024年诺贝尔物理学奖被授予了机器学习与神经网络领域的研究者。这是诺贝尔物理学奖历史上首次对非自然现象的科技研究给予如此高的荣誉。此举不仅彰显了机器学习和神经网络在科学领域的重要性,也预示着这两大领域将在未......
  • 流量抓包和网络问题排查,网工不要只会Wireshark,用好TCPdump才是大神!
    你好,这里是网络技术联盟站,我是瑞哥。在网络工程师的日常工作中,流量抓包和网络问题的排查是不可或缺的一环。Wireshark作为图形界面强大的流量分析工具,深受众多网络工程师的喜爱。然而,仅仅依赖Wireshark并不足以成为一个真正的网络排查高手。真正的网络大神往往能熟练运......
  • Windows 通过私钥远程连接 Linux 服务器【含密钥对制作】
    在现代软件开发和系统管理中,远程连接Linux服务器是非常常见的任务。尤其在Windows系统下,使用SSH工具连接Linux服务器是开发者们不可或缺的技能之一。为了保证安全性,SSH密钥对(公钥和私钥)的使用可以避免传统用户名密码方式的安全风险,提供了更高的安全保障。本文将详......
  • Linux系统MySQL安装
    1.下载安装包官方网站:https://www.mysql.com/,找到下载DOWNLOADS,下载操作系统对应的社区版本。本文使用的数据库版本是5.7.41。在社区版本下载界面可以下载最新和以前的版本。2、安装MySQL2.1、查看是否已经安装MySQLrpm-qa|grepmysqlmysql-libs-5.1.73-7.el6.x86_64......
  • Linux模块
    ansible-doc-l:查看ansible系统的模块ansible-doc加模块名:具体查看那个模块ansible-doc-s加模块名:具体查看那个模块ansible重要常用模块命令模块:commandshellscript文件模块:filecopy安装模块:yum服务模块:service定时模块:cron挂载模块:mo......
  • Linux安装配置NFS实现目录挂载
    什么是NFSNFS(NetworkFileSystem)即网络文件系统,它允许网络中的计算机之间通过网络共享资源。将NFS主机分享的目录,挂载到本地客户端当中,本地NFS的客户端应用可以透明地读写位于远端NFS服务器上的文件,在客户端端看起来,就像访问本地文件一样。RPC,基于C/S模型。程序可以使用这个协......
  • Linux期末考试选择题题库
    在创建Linux分区时,一定要创建(D )两个分区A. FAT/NTFS  B. FAT/SWAP  C. NTFS/SWAP  D.SWAP/根分区在Red Hat Linux 9中,系统默认的(A)用户对整个系统拥有完全的控制权。A. root  B. guest  C. administrator  D.supervistor.当登录Linux时,一个具有唯一......
  • linux系统有什么优缺点
    Linux系统的优缺点包括:1、高度可定制;2、安全性高;3、硬件兼容性强;4、学习曲线陡峭;5、软件生态相对较弱。Linux系统以其开源和自由的特点吸引了大量的开发者和企业用户。它提供了强大的安全性和硬件兼容性,但同时,新手可能会觉得学习曲线陡峭。此外,与Windows和macOS相比,它的软件生态......
  • Linux内核文件系统-虚拟文件系统-文件操作
    建议点击这里查看个人主页上的最新原文作者:陈孝松主页:chenxiaosong.com哔哩哔哩:陈孝松课程:chenxiaosong.com/courses博客:chenxiaosong.com/blog贡献:chenxiaosong.com/contributions邮箱:[email protected]交流群:544216206,点击查看群介绍点......