当前位置: 首页 > news >正文

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

目录

一、传输控制协议TCP:

二、结构化数据协议:

二、序列化与反序列化:

三、网络版计算器服务端:

封装Socket:

TCP服务器代码:

协议代码:

计算器服务端处理报文:

启动服务端:

四、网络版计算器客户端:

五、JSON的使用:


一、传输控制协议TCP:

TCP是传输控制协议:能够传输数据,并且能够控制发送的数据

在我们使用write和read这样的接口的时候,实际上是进行数据的拷贝

在TCP层的用户间数据的传输的时候,用户是不必在意是怎么传输的,这是由TCP自己控制的,这点就体现了TCP的控制,并且TCP也会解决在传输过程中出现的问题,比如说发送的数据大小,发送的数据时间,发送出错后的处理方式等等这些问题都是由TCP自己解决的,用户不用管

并且TCP是OS的一部分,所以我们把数据交给TCP就是交给OS进行处理,此时OS进行处理会比我们用户自己处理更安全更高效

那么对于发送没有问题,那么对于读取却又点问题

我们知道TCP是面向字节流的

当我们通过read从接收缓冲区中进行读取,此时就有问题了,接收缓冲区中可能有发送过来的一组或者半组或者多组数据,那么read要怎么保证读上来的数据是可靠的呢?会不会读了半组数据,或者多组数据,这些字节流要怎么进行分开呢?

对于上述这个问题,我们就需要在应用层制定协议,读端根据协议将读上来的数据进行解析,这样就能够获得完整的一组数据了

综上所述:

用户发送端是通过调用write接口将数据拷贝给TCP层的发送缓冲区中,此时由TCP自己控制,用户层就不用管,TCP会自己将数据发送到接收方的接收缓冲区,至于什么时候发,发多少等等是由TCP自己决定的,用户就不用管了

接收端就会从它自己的接收缓冲区接收数据,此时用户就会在接收缓冲区中读取数据,这个时候双方会定制好的专属协议就会生效,当接收端的用户层将数据通过read读取过去后,就会通过协议解析数据,进而实现双方数据的交流

二、结构化数据协议:

当双方进行通信的时候,如果传输字符串,那么发送到网络中,之后对端能够从网络中读取,这是没问题的,但是如果发送的是结构化的数据,就不能够简单的将结构体传输到对端,需要进行一个约定,也就是协议

比如在网络版本的计算器中,就会有如下结构化协议:

Request
{int x; // 左运算数int y; // 右运算数int op; // 运算符
};Response
{int result; // 结果int code; // 结果返回值,用这个判断结果可不可信
};

但是在网络中是不能够直接传输这样的结构体的,因为存在内存对齐,不同的OS中,对结构体的大小解析是不一样的,可能在Linux这是20字节,到Windows中就成了15字节,那么也就肯定出问题了

二、序列化与反序列化:

  • 序列化:把内存中的数据结构转化为一种可存储或传输的格式的过程
  • 反序列化:与序列化相反,是将序列化后的数据重新还原成内存中数据结构的过程

比如在我们QQ正常发送消息过程中,发送的不仅只是想要发送的消息,还有我的昵称,发送时间

那么就会有如下结构化的数据协议:

struct message
{string info;string time;string name;
}

那么在网络中进行发送消息的时候,发送的不会是这个结构体一般的消息,而是序列化之后成为一串字符串的消息,将这个字符串的消息在网络上发送到对端,然后对端进行反序列化,将字符串数据读取上来,这样就能够实现网络间通信了,即使OS不一样

双方规定相同的结构体,也就是相同的结构化对象,这其实就是一个双方规定的协议,将这个message这样的结构体对象转化成一个字符串,这就是序列化,这样方便在网络中进行收发消息,当数据传送到对端的时候,对端的应用层在将这个字符串从OS中的接收缓冲区中将数据读取上来,在进行将这个字符串转化成结构体对象,这样的操作就叫做反序列化

三、网络版计算器服务端:

接下来根据上述的结构化数据协议和序列化反序列化的知识写一个网络版本的计算器

封装Socket:

日志部分代码用的就是之前的日志代码

这里的Socket.hpp就是网络中的接口封装,比如创建套接字,绑定套接字,监听,连接,接收

#include <iostream>
#include <unistd.h>
#include <string.h>#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>#include "log.hpp"enum{SOCKET_ERR = 2,BIND_ERR,LISTEN_ERR
};const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}
public:void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM,0);if(_sockfd < 0){lg(FATAL,"socket err,%s:%d",strerror(errno),errno);exit(SOCKET_ERR);}}void Bind(uint16_t port){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;socklen_t len = sizeof(local);int n = bind(_sockfd,(struct sockaddr*)&local,len);if(n < 0){lg(FATAL,"bind err,%s,%d",strerror(errno),errno);exit(BIND_ERR);}}void Listen(){int n = listen(_sockfd,backlog);if(n < 0){lg(FATAL,"listen err,%s,%d",strerror(errno),errno);exit(LISTEN_ERR);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in client;socklen_t len = sizeof(client);int n = accept(_sockfd,(struct sockaddr*)&client,&len);if(n < 0){lg(WARNING, "accept error, %s: %d", strerror(errno), errno);return -1;}char in_buffer[64];inet_ntop(AF_INET,&client,in_buffer,sizeof(in_buffer));*clientip = in_buffer;*clientport = ntohs(client.sin_port);return n;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));socklen_t len = sizeof(server);int n = connect(_sockfd,(struct sockaddr*)&server,len);if(n < 0){std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(_sockfd);}int Getsockfd(){return _sockfd;}public:int _sockfd;
};

TCP服务器代码:

构造函数这里有两个参数,分别是连接的端口号和一个回调函数,回调函数这里使用的是Function包装器,这里绑定的是服务端的计算函数实现一个回调

#pragma once#include <iostream>
#include <signal.h>
#include <string>
#include <functional>#include "Socket.hpp"using func_t = std::function<std::string(std::string &package)>;class TcpServer
{
public:TcpServer(){}TcpServer(uint16_t port, func_t callback) : _port(port), _callback(callback){}void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();lg(INFO, "init server .... done");}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;lg(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;lg(DEBUG, "debug:\n%s", inbuffer_stream.c_str());while (true){std::string info = _callback(inbuffer_stream);if (info.empty())break;lg(DEBUG, "debug:response:\n%s", info.c_str());lg(DEBUG, "debug:\n%s", inbuffer_stream.c_str());write(sockfd, info.c_str(), info.size());}// std::string info = _callback(inbuffer_stream);// // 判空// if (info.empty())//     continue;// // 写入数据// write(sockfd, info.c_str(), info.size());}else if (n == 0)break;elsebreak;}exit(0);}close(sockfd);}}~TcpServer(){}public:int _port;Sock _listensock;func_t _callback;
};

协议代码:

这里封装了两个结构体,分别是请求的计算数据Request和返回的结果Response,在Request和Response分别进行序列化和反序列化,将1+1这样的从结构体变为字符串,将返回的结果result结果和code返回值,从结构体中的成员变量序列化为字符串

然后还有就是添加报头和解析报头独立于结构体中,这是方便我们后面用JSON进行序列化的,用JSON的时候就只需修改序列化那里的代码了

这里报头的格式是"len"\n"x op y"\n,通过\n为分割符,这样每当收到一组数据的时候,就能够通过解析报头知道传输过来的数据有多长,然后直接通过长度截取数据,这样即使读上来的数据是半组或者一组或者多组,都不会出问题的:半组的话,发现长度不够返回false,多组的话,我们是通过长度截取的,就不会截取多余的部分

#include <iostream>
#include <string>// 分割符
const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";// 添加报头
// "len"\n"x op y"\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"\n -> "x op y"
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 = stoi(len_str);// 判断报头是否符合要求std::size_t total_len = len + len_str.size() + 2;if (package.size() < total_len)return false;//std::cout << "移除报文成功" << std::endl;*content = package.substr(pos + 1, len);package.erase(0,total_len);return true;
}class Request
{
public:Request(int data1, int data2, char oper): _x(data1), _y(data2), _op(oper){}Request(){}// 构建报文有效载荷// struct -> string,"x op y"bool Serialize(std::string *out){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;}// 提取有效载荷,反序列化// "x op y" -> structbool Deserialize(const std::string &in){// 提取左边size_t left = in.find(blank_space_sep);if (left == std::string::npos)return false;std::string part_x = in.substr(0, left);// 提取右边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;// 填充结构体_x = stoi(part_x);_y = stoi(part_y);_op = in[left + 1];return true;}void DebugPrint(){std::cout << "新请求构建完成:  " << _x << _op << _y << "=?" << std::endl;}public:int _x;int _y;char _op;
};class Response
{
public:Response(int res, int code): _result(res), _code(code){}Response(){}// 构建报文有效载荷// struct -> string,"_result _code"bool Serialize(std::string *out){std::string s = std::to_string(_result);s += blank_space_sep;s += std::to_string(_code);*out = s;return true;}// 反序列化// string,"_result _code" -> structbool Deserialize(const std::string &in){std::size_t res = in.find(blank_space_sep);if (res == std::string::npos)return false;std::string left = in.substr(0, res);std::string right = in.substr(res + 1);_result = std::stoi(left);_code = std::stoi(right);return true;}void DebugPrint(){std::cout << "结果响应完成, result: " << _result << ", code: "<< _code << std::endl;}public:int _result;int _code;
};

计算器服务端处理报文:

这个是如何处理整个报文的,其中callback回调也是回调的这里面的Calculator方法,这是进行数据处理的,当执行流执行到这个方法的时候,此时肯定是收到了已经封装好报头的消息。所以第一步是解析报头,接着对请求进行反序列化,然后进行数据处理,将处理好的数据放到结构体Response中,进行序列化,添加报头后发送到网络中

// 如何处理整个报文
#pragma once#include <iostream>
#include "Protocal.hpp"enum
{Div_Zero = 1,Mod_Zero,unKnow
};
class ServerCal
{
public:ServerCal(){}Response CalculatorHelper(const Request &req){Response res(0, 0);switch (req._op){case '+':res._result = req._x + req._y;break;case '-':res._result = req._x - req._y;break;case '*':res._result = req._x * req._y;break;case '/':{if (req._y == 0){res._code = Div_Zero;break;}res._result = req._x / req._y;break;}case '%':{if (req._y == 0){res._code = Mod_Zero;break;}res._result = req._x % req._y;break;}default:res._code = unKnow;break;}return res;}std::string Calculator(std::string &package){// 此时肯定是收到了已经封装好报头的消息// 所以第一步是解析报头Request req;std::string content;//std::cout << "content:" << content << "xxxxx" << std::endl;int r = Decode(package, &content);//std::cout << "解码后content:" << content << "xxxxx" << std::endl;if (!r)return "";// 进行反序列化r = req.Deserialize(content);if (!r)return "";// 进行数据处理Response res = CalculatorHelper(req);// 序列化content = "";res.Serialize(&content);// 封装报头content = Encode(content);//  std::cout << "构造后的响应content:" << content << "xxxxx" << std::endl;return content;}~ServerCal(){}
};

启动服务端:

#include "TcpServer.hpp"
#include "ServerCal.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc,char* argv[])
{// 手册if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = stoi(argv[1]);ServerCal cal;TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tsvp->InitServer();tsvp->Start();return 0;
}

这样,我们服务端的代码就写好了

四、网络版计算器客户端:

接着实现客户端,这里就不进行封装了

客户端我们运行的时候,在命令行通过如下方式:./clientcal serverip serverport,所以就得到了服务端的IP地址和端口号

接着就是常规的网络通信前置操作:

创建sockfd套接字,bind绑定(这步是由OS做的),connect链接

然后在客户端中创建好Request请求计算式,然后进行序列化,添加报头,然后将请求发送到服务端中的接收缓冲区中,服务端处理完数据后就能够发送回客户端的接收缓冲区中,接着客户端就能够从自己的接收缓冲区中进行读取,并且用一个buffer接收,然后解析报头,反序列化用Response结构体实例化的对象接受,这样就拿到了处理后的数据了

#include <iostream>
#include <ctime>
#include <cassert>
#include "Protocal.hpp"
#include "Socket.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./ClienCal serverip serverport
int main(int argc, char *argv[])
{// 手册if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 创建sockfdSock sockfd;sockfd.Socket();// connect连接bool r = sockfd.Connect(serverip, serverport);if (!r)return 1;// 随机srand(time(nullptr) ^ getpid());// 进行cnt次测试int cnt = 1;const string oper = "+-*/%=#";std::string buffer_stream; // 这个是不是必须放在while循环外面?while (cnt <= 10){std::cout << "============" << "第" << cnt++ << "次测试" << "================" << std::endl;// 创建x和y和op符号int x = rand() % 100 + 1;// cout<<"x"<<x<<endl;usleep(1234);int y = rand() % 10;// cout<<"y"<<y<<endl;usleep(5678);char op = oper[rand() % oper.size()];// cout<<"op"<<op<<endl;// 初始化req对象Request req(x, y, op);req.DebugPrint();std::string content;// 序列化r = req.Serialize(&content);// 添加报头content = Encode(content);// 发送信息//  std::cout << "content:" << content << "xxxxx" << std::endl;write(sockfd.Getsockfd(), content.c_str(), content.size());// write(sockfd.Getsockfd(), content.c_str(), content.size());// write(sockfd.Getsockfd(), content.c_str(), content.size());// write(sockfd.Getsockfd(), content.c_str(), content.size());// write(sockfd.Getsockfd(), content.c_str(), content.size());// std::cout << "=================================================" << std::endl;// 用buffer读取信息char buffer[128];ssize_t n = read(sockfd.Getsockfd(), buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;buffer_stream += buffer;// 解析报头std::string content;// std::cout << "读到的响应" << buffer_stream << std::endl;//  将buffer_stream也就是收到的服务端发来的所有信息,提取报文,解析报头Decode(buffer_stream, &content);//  std::cout << "解码响应" << buffer_stream << std::endl;// 反序列化Response res;res.Deserialize(content);res.DebugPrint();}std::cout << "=================================================" << std::endl;sleep(1);// break;}// 关闭sockfdsockfd.Close();return 0;
}

五、JSON的使用:

如果每次都要我们自己写序列化和反序列化是很麻烦的,但是在库里面有直接能够进行序列化和反序列化的接口供我们使用:

首先想要使用JSON库就需要先安装第三方库

sudo apt-get install libjsoncpp-dev

安装后查看如下下路径是否存在文件,如果是像下述这样的证明成功安装JSON

测试JSON:

#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>using namespace std;int main()
{Json::Value part; // 被嵌套的JSONpart["desc1"] = "haha";part["desc2"] = "hehe";Json::Value root; // JSON里面也能够嵌套JSONroot["x"] = 100;root["y"] = 200;root["op"] = '+';root["desc"] = "this is a + oper";root["part"] = part;// 如下的FastWrite和StyleWrite是两种不同风格的JSONJson::FastWriter w;// Json::StyledWriter w;string res = w.write(root); // 序列化,参数就是我们的rootcout << res << endl;Json::Value v;Json::Reader r; // r就是一个方法,利用r里面的方法将res字符串给v,就完成了反序列化r.parse(res, v);int x = v["x"].asInt();int y = v["y"].asInt();int op = v["op"].asInt();string desc = v["desc"].asString();return 0;
}

FastWrite风格的JSON

StyledWriter风格的JSON

那么我们可以将自己的序列化,反序列化代码替换成库里面的了

这里使用一下条件编译,这样能够快速地切换自己写的版本和JSON版本了

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>// #define Myself 1// 分割符
const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";// 添加报头
// "len"\n"x op y"\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"\n -> "x op y"
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 = stoi(len_str);// 判断报头是否符合要求std::size_t total_len = len + len_str.size() + 2;if (package.size() < total_len)return false;// std::cout << "移除报文成功" << std::endl;*content = package.substr(pos + 1, len);package.erase(0, total_len);return true;
}class Request
{
public:Request(int data1, int data2, char oper): _x(data1), _y(data2), _op(oper){}Request(){}// 构建报文有效载荷// struct -> string,"x op y"bool Serialize(std::string *out){
#ifdef Myselfstd::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;*out = w.write(root);return true;#endif}// 提取有效载荷,反序列化// "x op y" -> structbool Deserialize(const std::string &in){
#ifdef Myself// 提取左边size_t left = in.find(blank_space_sep);if (left == std::string::npos)return false;std::string part_x = in.substr(0, left);// 提取右边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;// 填充结构体_x = stoi(part_x);_y = stoi(part_y);_op = in[left + 1];return true;
#elseJson::Value root;Json::Reader rd;rd.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:int _x;int _y;char _op;
};class Response
{
public:Response(int res, int code): _result(res), _code(code){}Response(){}// 构建报文有效载荷// struct -> string,"_result _code"bool Serialize(std::string *out){
#ifdef Myselfstd::string s = std::to_string(_result);s += blank_space_sep;s += std::to_string(_code);*out = s;return true;#elseJson::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter w;*out = w.write(root);return true;
#endif}// 反序列化// string,"_result _code" -> structbool Deserialize(const std::string &in){
#ifdef Myselfstd::size_t res = in.find(blank_space_sep);if (res == std::string::npos)return false;std::string left = in.substr(0, res);std::string right = in.substr(res + 1);_result = std::stoi(left);_code = std::stoi(right);return true;#elseJson::Value root;Json::Reader rd;rd.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;
};

当定义宏Myself=1的时候用的就是自己编写的序列化和反序列化,当没有定义Myself的时候用的就是系统给的JSON序列化和反序列化

所以Makefile也要跟着修改

.PHONY:all
all:servercal clientcalFlag=-DMyself=1
Lib=-ljsoncppservercal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)clientcal:ClientCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag).PHONY:clean
clean:rm -f servercal clientcal

如下是自己版本的

如下是JSON版本的

相关文章:

  • 组件化设计核心:接口与实现分离(C++)
  • JAVA学习-练习试用Java实现“TensorFlow/Deeplearning4j:利用DL4J构建卷积神经网络进行图像分类”
  • ios签名错误的解决办法
  • 百胜软件胜券AI:打造智慧零售运营新范式
  • 布瑞琳BRANEW:高端洗护领航者,铸就品质生活新典范
  • TestCafe 全解析:免费开源的 E2E 测试解决方案实战指南
  • 【C#】C#异步编程:异步延时 vs 阻塞延时深度对比
  • wsl2 用桥接方式连网
  • 错误: 程序包androidx.fragment.app不存在 import android
  • Linux切换中文输入法
  • 商品中心—11.商品B端搜索系统的实现文档二
  • 腾讯云 CodeBuddy 技术评估报告(2025年):编码效率提升40%,复杂工程处理能力领先Cursor 35%​
  • idea2024里的jar打包(找不到主类解决方法)
  • idea依赖下载慢解决
  • 图形化http api测试工具yunedit-post
  • Web基础 -SpringBoot入门 -HTTP-分层解耦 -三层架构
  • 利用栈,实现括号匹配功能
  • vtkImageData去噪——vtkImageMedian3D
  • 板凳-------Mysql cookbook学习 (十--9)
  • 带约束的高斯牛顿法求解多音信号分离问题
  • 免费的建设网站软件下载/搜索引擎官网
  • 企业网页制作哪家公司好/seo整站优化一年价格多少
  • 做短视频网站好/快速排名优化推广手机
  • 老师做家教的网站/googleplay官网
  • 协助别人做网站犯法么/爱站网关键词查询工具
  • 机械毕业设计代做网站/google关键词工具