序列与反序列化

目录

TCP的发送和接收过程理解

序列化与反序列化

Jsoncpp

简介

序列化

反序列化

自定义协议格式


TCP的发送和接收过程理解

如下图所示:

之前我们写的EchoServer和DictServer都属于应用层的编写,传输层往上,都属于用户区域,传输层及其往下的部分,都属于内核区域。

通信时,用户可以使用read、write等函数,以TCP通信的一个文件描述符为例,在传输层会为通信双方提供一个发送缓冲区和接收缓冲区。当发送者发送数据时,并不是直接将相关数据直接发送到网络中,而是通过write函数将用户空间拷贝到发送缓冲区中,并返回;而发送缓冲区的数据如何发送、何时发送以及出错的处理方式,都是由传输层TCP协议自己维护;因此,write函数本质是一个拷贝函数,将用户的数据拷贝到对应的发送缓冲区中;随后,TCP协议将发送缓冲区中的数据发送到对方的接收缓冲区中,接收方收到数据会将数据拷贝到自己的接收缓冲区中,当用户调用read接口时,如果接收缓冲区中有数据,read就将数据拷贝到用户空间中,因此,read函数本质上也是一个拷贝函数;如果没有数据,read就会阻塞;如果在发送方发送数据过快,发送缓冲区被写满了,此时write就会阻塞;因此,在通信时,双方使用的函数都是在进行拷贝,真正在网络中通信的,是双方的操作系统。

这也是TCP为什么支持全双工通信的原因,因为通信双方有一对缓冲区,进行发送的时候可以接收,接受的时候也可以发送;同时,用户拷贝数据到内核对应的发送缓冲区,用户拷贝接收缓冲区中的数据到用户空间,是个"生产者-消费者"模型,这也是read和write会什么会阻塞的原因,阻塞的原因是用户在和内核进行同步。操作系统可能会收到很多个报文,也会有还没来得及放入到缓冲区当中的报文,因此,操作系统也要对收到的报文进行管理,管理的方式同样是"先描述,再组织",每个报文都会有对应的数据结构。

但是在这个发送过程中,也会存在一定的问题,例如,当用户发送命令"ls -l -a"到服务器中,TCP可能只会先将"ls"发送,过一段时间才发送"-l -a",这就会导致命令执行出现问题,读数据时,有可能读到的数据只是发送的一部分,读取的过程和写入的过程不一定是完全匹配的,这种读取不完整的报文叫做数据粘包问题。保证完整新的过程是要上层用户自己来实现的,这时就需要定义用户协议了,比如,规定读到'\0'才算结束等规则。

序列化与反序列化

除了传基本数据进行通信,还可以定义结构体来表示我们需要交互的信息,但是在应用层,不推荐直接使用结构体变量的方式进行通信,这是因为客户端和服务端可能在不同的系统行运行,可能存在不同程度的对齐问题以及大小端问题,此时传结构体必须要人为固定大小才能正常通信,同时,通信的双方不一定是同一门编程语言,这时传结构体也不方便通信,最好的实现方式是序列化与反序列化。

序列化与反序列化的大致过程如下所示:

在聊天的过程中,发送消息时,可将不同消息变成一条,将信息由多条变为一条,是序列化的过程,对方收到信息,在应用层将收到的信息转换成多条信息,是反序列化的过程;序列和反序列化可以让我们写出来的代码具有很好的扩展性。

Jsoncpp

简介

Jsoncpp是⼀个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串 反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数的C++项目中。可使用如下命令进行安装:

bash 复制代码
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

序列化

Json中的Value是一个万能对象,通过toStyledString函数可以将对象直接转成格式化的Json字符串。如下所示:

cpp 复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
 Json::Value root;
 root["name"] = "joe";
 root["sex"] = "男";
 std::string s = root.toStyledString();

 std::cout << s << std::endl;

 return 0;

}

$ ./test.exe 
{
 "name" : "joe",
 "sex" : "男"
}

也可以使用FastWriter和StyleWriter对象进行序列化,如下面两个程序:

cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
 Json::Value root;
 root["name"] = "joe";
 root["sex"] = "男";
 Json::FastWriter writer;
 std::string s = writer.write(root);
 std::cout << s << std::endl;

 return 0;

}

$ ./test.exe 
{"name":"joe","sex":"男"}
cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
 Json::Value root;
 root["name"] = "joe";
 root["sex"] = "男";
 // Json::FastWriter writer;
 Json::StyledWriter writer;
 std::string s = writer.write(root);
 std::cout << s << std::endl;

 return 0;

}


$ ./test.exe 
{
 "name" : "joe",
 "sex" : "男"
}

同时,也可以使用StreamWriter进行序列化,如下程序所示:

cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "zhangsan"; //key:value
    root["sex"] = "man";
    root["age"] = 18;

    Json::StreamWriterBuilder wbuilder;
    std::unique_ptr<Json::StreamWriter> swriter(wbuilder.newStreamWriter());

    std::stringstream ss;
    swriter->write(root, &ss);

    std::cout << ss.str() << std::endl;

    return 0;
}

// ./a.out
{
        "age" : 18,
        "name" : "zhangsan",
        "sex" : "man"
}

由于JsonValue串是个万能对象,不仅可以接收基本类型,还可以接收自己,如下程序所示:

cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value sub_root1;
    sub_root1["name"] = "zhangsan";
    sub_root1["sex"] = "man";
    sub_root1["age"] = 18;

    Json::Value sub_root2;
    sub_root2["math"] = 88;
    sub_root2["chinese"] = 98;

    Json::Value root;
    root["who"] = sub_root1;
    root["score"] = sub_root2;
    Json::StreamWriterBuilder wbuilder;
    std::unique_ptr<Json::StreamWriter> swriter(wbuilder.newStreamWriter());

    std::stringstream ss;
    swriter->write(root, &ss);

    std::cout << ss.str() << std::endl;


    return 0;
}

// ./test.exe
{
	"score" : 
	{
		"chinese" : 98,
		"math" : 88
	},
	"who" : 
	{
		"age" : 18,
		"name" : "zhangsan",
		"sex" : "man"
	}
}

反序列化

反序列化的方式也有多种,这里我们介绍Json::Reader,得到序列化的字符串后,可以通过parse函数将JSON串解析成value对象,读取成功后通过key访问数据即可。如下程序所示:

cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>

int main()
{
    std::string json_string = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";

    std::cout << json_string << std::endl;

    Json::Reader reader;
    Json::Value root;
    bool parseok = reader.parse(json_string,root);

    if(!parseok)
    {}

    std::string name = root["name"].asString();
    int age = root["age"].asInt();
    std::string city = root["city"].asString();

    std::cout << name << std::endl;
    std::cout << age << std::endl;
    std::cout << city << std::endl;

    return 0;
}

自定义协议格式

除了解决序列化的问题,还要定制完整的协议格式,以确保接收方接收的信息是完整的,报文格式如下图所示:

报文前面4字节表示简单的报头,代表有效载荷长度,中间的有效载荷为序列化之后的字符串,当接收方读取报文时首先读取有效载荷的长度,并读取对应大小的内容作为有效载荷的内容,以此来解决粘包问题。

相关推荐
the sun342 小时前
计算机网络:数据链路层协议
网络·网络协议·计算机网络
小川zs2 小时前
OpenClaw Gateway 频繁断开/重启问题诊断
linux·服务器·gateway
Mr数据杨2 小时前
【Dv3Admin】FastCRUD MD编辑器操作
服务器·网络·编辑器
程序员一点2 小时前
第23章:备份与灾难恢复策略
linux·运维·网络·数据库·openeuler
IT WorryFree2 小时前
openclaw接入企业飞书机器人:个人增效和团队自动化两种场景
服务器·自动化·飞书
静候光阴2 小时前
为本地ollama设置网页搜索mcp服务器
运维·服务器
ฅ^•ﻌ•^ฅ12 小时前
LeetCode hot 100(复习c++) 1-15
c++·算法·leetcode
艾莉丝努力练剑2 小时前
确保多进程命名管道权限一致的方法
java·linux·运维·服务器·开发语言·网络·c++
Z9fish2 小时前
sse哈工大C语言编程练习44
c语言·c++·算法