序列化和反序列化

devtools/2024/12/22 21:17:33/

一 概念理解

        先前已经可以利用sock套接字通信了,但是数据如何处理就是我们应用层协议的内容了,之前都是发送一些字符串,但是实际上我们发送的消息可能是个结构化的数据。

        那我们能不能直接发结构体呢? 可以但是会浪费空间,你想想我们平时写的作文有固定格式和缩进,但是对于网络来说这些缩进是浪费空间,所以我们序列化是为了压缩发送的数据大小。 

        将结构化的数据转为一个大字符串,称为序列化,然后发给服务端,服务端解析字符串(这就是反序列化),然后服务端构建响应,又序列化发给客户端。

        我们是凭什么对一个结构体序列化,对字符串反序列的,就是双方约定好了一个格式,这样才能解析发来的数据,我们在下面约定的格式就是一种协议,而且是应用层协议。

        之前我们只是用了一下socket接口,根本没有对数据做序列化和反序列,也没有对数据做处理,因为无场景。接下来我们实现一个网络版本的计算器,从中我们会设计序列化和反序列化,本质是设计一个应用层协议。

二 编码实现

        计算器客户端和服务端实现放在两个.cc文件,通信客户端和服务端实现放在了头文件中。

1 makefile

        makefile:一同编译client.cc和server.cc。

.PHONY:all
all:client server
client:CalculatorClient.ccg++ -o $@ $^ -std=c++11 -lpthread
server:CalculatorServer.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -rf server client

2 封装系统调用

        由于我们要进程调用和套接字相关的接口,所以就对这些接口做了封装。

class Sock
{public:int socket_;Log log_;
};

         不用cout,而是用日志打印,日志打印是我们之前封装的模块,下面直接展示代码,使用的时候我们直接用log_调用仿函数就可以了。

enum ErrorLevel
{Info = 1,Warning,Fatal,Debug
};
//接收输出的文件
enum PMethod
{Screen = 1,//输出到屏幕OneFile ,//输出到一个文件上ClassFile//分类输出到多个文件中
};class Log
{
public:Log(int method = Screen):printmethod(method){;}string leveltostring(int level){switch (level){case Info:return "Info";case Warning:return "Warning";case Fatal:return "Fatal";case Debug:return "Debug";default: return "None";    }}//日志信息void operator()(int level, const char *format, ...){char leftbuffer[SIZE];time_t t = time(NULL);struct tm * ltime = localtime(&t);//默认部分 事件等级和时间snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);//可变部分char rightbuffer[SIZE];va_list s;va_start(s,format);vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);// printf("%s %s\n",leftbuffer,rightbuffer);char Logbuffer[SIZE*2];snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);LogPrint(level,Logbuffer);}void PrintOnefile( const char *filename,string& lbuffer){lbuffer+='\n';int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);if(fd < 0)return;write(fd,lbuffer.c_str(),lbuffer.size());   }void PrintClassFile(int level,string& lbuffer){string filename = Logname;//将不同错误信息分流到对应的文件filename += ".";filename += leveltostring(level);PrintOnefile(filename.c_str(),lbuffer);}void LogPrint(int level,string lbuffer){switch(printmethod){case Screen://输出到屏幕cout<<lbuffer<<endl;break;case OneFile: //输出到一个文件上PrintOnefile(Logname,lbuffer);break;case ClassFile:PrintClassFile(level,lbuffer);break;}}
private:int printmethod;
};

        之前创建套接字通信的时候,我们定义了许多和错误信息相关的宏,现在直接拿来用。

        接下来才到封装的实现。

创建套接字

绑定

监听

接收链接

     int Accept(string* ip,uint16_t* port) // 名字不能为accept{// 获取链接struct sockaddr_in sock; // 头文件<netinet/in.h>bzero(&sock, sizeof(sock));socklen_t len = sizeof(sock);int socket = accept(socket_, (sockaddr *)&sock, &len);if (socket < 0){log_(ErrorLevel::Info, "accept err");exit(SOCKET_ERR);}else{*ip = inet_ntoa(sock.sin_addr);*port = ntohs(sock.sin_port);}return socket;}

        connect,这个connect是客户端要用的,也是sock接口,就一同封装了。

        Sock类中的套接字socket_含义由使用者来定义,可以是监听套接字,也可以是直接写的套接字。

三 服务端实现

        我们是对服务端实现做了封装,封装在了该头文件中。

        服务端main函数在该文件中。

        我们在main函数中给服务类传端口号和可调用对象,服务端ip不用绑定,我这是云服务器,一绑定就会出错,然后初始化和启动服务端。

        接下来看看服务器内部实现。成员如下,func_是接收可调用对象的。

1 初始化

        初始化显然就是创建和监听套接字,显然我们此时的通信是基于tcp协议的。

2 初识start

        暂时没链接时会链接失败,此时我们不能直接退出,要继续链接。

    class Tcpserver;class ThreadData{public:ThreadData(std::string ip, uint16_t port,int socket,Tcpserver* ts):ip_(ip),port_(port),socket_(socket),ts_(ts){;}std::string ip_;uint16_t port_;int socket_;Tcpserver* ts_;}; class Tcpserver{public:using func_t = std::function<Response(const Request)>;Tcpserver(func_t func, uint16_t port): port_(port), func_(func){;}void start() // 接收链接,获取客户端端口号+ip,创建线程执行{while (true){string ClientIp;uint16_t clientport;int socket_ = sock_.Accept(&ClientIp, &clientport);if (socket_ < 0)continue;log_(ErrorLevel::Debug, "get a new client,client info:            [%s:%d]",ClientIp.c_str(),clientport);创建线程,传递客户端端口号和ippthread_t id;ThreadData *td = new ThreadData(ClientIp, clientport, socket_, this);pthread_create(&id, nullptr, threadRoutine, td);}}private:func_t func_;Log log_;Sock sock_;uint16_t port_;};

        我们给执行函数传了个ThreadData类对象。在threadRoutie掉用serverio函数,方便后续如果服务端收到请求要做其它处理,此时就将serverio函数换成其它函数即可。

        static void* threadRoutine(void*arg){ThreadData* td = static_cast<ThreadData*>(arg);td->ts_->ServerIO(td->ip_,td->port_,td->socket_);}

         我们知道如果ServerIO函数要和客户端通信,肯定需要客户端ip,端口和套接字,所以我们先把这些参数合并传给该函数。写完后,但是客户端还没写好,如何测试,我们可以用可以指令向服务器发起一个链接。

         void ServerIO(std::string ip, uint16_t port,int socket){;}

        可以测试目前代码是否可以跑通,也就是套接字创建是否会问题。

        在实现ServerIO函数前,我们先定协议,先记住我们要在这个函数读客户端消息,处理数据并返回,这个大致步骤方便我们实现完协议来理解调用逻辑。

3 设计协议

        数据的流动前面提过:客户端发起request,序列化转为字符串发给服务端,服务端收到后反序列化,处理返回responce,将结果序列化再发给客户端,客户端收到后再反序列化转为Response对象,客户端直接读写对象成员就可以获得结果了。

        所以我们要将request和Response序列化和反序列化。封装在下面这个头文件中。

Request序列化

        协议就是规定:我们首先规定Request请求就是x + y,两个操作数,一个操作符,然后序列化的字符串必须是"x + y",有时候我们想让字符串变成"x  +  y",操作符之间间隔增加一个空格,这里要体会一下不用宏的话,如果要变更格式有多麻烦。

 #define SEP " "#define SEP_LEN strlen(SEP) 
class Request{public:Request(){;}Request(int x, int y, char op): x_(x), y_(y), op_(op){;}// class -> string 序列化bool serialize(std::string *res){*res += std::to_string(x_);*res += SEP;*res += op_;*res += SEP;*res += std::to_string(y_);return true;}~Request(){;}int x_;int y_;char op_;};

反序列化

        既然前面已经规定操作数之间,操作符和操作数之间是有间隔符的,那我们就用string的find接口查找间隔符SEP,把一个个操作数截取下来。

class util
{
public://"10 + 10"static void StringSplit(const std::string res,const std::string sep,std::vector<std::string>*vs){int pos = 0;while(pos != -1){int nextpos = res.find(sep.c_str(),pos);if(nextpos == -1)break;vs->push_back(res.substr(pos,nextpos - pos));pos = nextpos + sep.size();}截取最后一个操作数vs->push_back(res.substr(pos,-1));}
};

        将"x + y"截取成"x","+","y"保存到vector中,此时我们可以更深刻的意识到协议就是规定。

         string -> class "x + y" 反序列化bool Deserialize(const std::string &res){std::vector<std::string> vs;util::StringSplit(res, SEP, &vs);if (vs.size() != 3)return false;x_ = atoi(vs[0].c_str());if (vs[1].size() != 1)return false;op_ = vs[1][0];y_ = atoi(vs[2].c_str());}

        做判断,操作符大小必须是1,操作符和操作数的大小和为3,这,都是基于我们的规定的来的。

Response 序列化

 class Response{public:Response(){;}Response(int result, int exitcode): result_(result), exitcode_(exitcode){;}// class -> stringbool serialize(std::string *msg){*msg += std::to_string(result_);*msg += SEP;*msg += std::to_string(exitcode_);return true;}~Response(){;}int result_;int exitcode_;};

        我们在序列化Respnse的时候规定了,必须是"结果 + 退出码",这是反序列的时候截取字符串的基础。

反序列化,还可以复用Reuest实现的截取字符串,封装的妙处总是在不经意中体现。

           string -> classbool Deserialize(const std::string &res){std::vector<std::string> vs;if (vs.size() != 2)return false;util::StringSplit(res, SEP, &vs);result_ = atoi(vs[0].c_str());exitcode_ = atoi(vs[1].c_str());return true;}

        可是设计了协议,怎么调用呢?调用顺序是什么呢? 我们前面说了,我们在threadRoutine内调用ServerIO函数来读写,所以接下来就在该函数内将上述实现用起来。

4 start完善

       先读数据,既然是读数据,自然是从套接字中读取,有意思的是我们怎么保证一次读一个完整报文呢,也就是说我们怎么保证一次读出"x + y"这样的完整报文,我们调用read每次读固定大小,肯定会出现读取多个报文的情况,所以我们对协议做了修改,原先规定请求是"x + y",为了方便读取,我们添加了一个报头,5"\n"x + y",然后设计一个函数,保证能切割出一个完整报文给下一步执行。这个还和tcp面向字节流有些关系,导致tcp向上向下交付以字节为单位,而不是一个一个数据报交付,需要我们手动切割。

        我们来看看函数内部实现。inbuffer保存了recv读到的所有数据。

        这个是后面经常用的宏。

     从socket读取数据,保存到inbuffer中,并解析出数据报放到package中int Readpackage(int socket, std::string *inbuffer, std::string *package){cout << "读取数据前:" << *inbuffer << endl;Log log_;char buffer[1024] = {0};int n = recv(socket, buffer, sizeof(buffer), 0);*inbuffer += buffer; 保存读到的数据cout << "读取数据中:" << endl<< *inbuffer;if (n < 0) // 出错后返回{return -1;}截取一个数据报 "5\n"x + y"\n"int pos = inbuffer->find(HEAD, 0);if (pos == -1){return -1;}// 截取的是记录着有效载荷长的字符串std::string Size = inbuffer->substr(0, pos);int lensize = atoi(Size.c_str()); 有效载荷长度完整报文长度int packagelen = lensize + 2 * HEAD_LEN + Size.size();if ((*inbuffer).size() < packagelen) 读取的数据不够一个数据报return 0;截取有效载荷*package = inbuffer->substr(pos + HEAD_LEN, lensize);inbuffer->erase(0, packagelen);cout << "读取数据后:" << endl<< *inbuffer;return lensize;}

        此时我们回到外部函数逻辑中。

        void ServerIO(std::string ip, uint16_t port, int socket){std::string inbuffer;while (true){1 读取数据std::string package;int n = Readpackage(socket,&inbuffer,&package);if (n < 0){    close(socket);exit(READ_ERR);}else if (n == 0)continue;到了这里就已经读取到了完整的数据报,先去除报头其实已经不用去除了,因为前面我们读取的就是一个有效载荷package = RemoveHead(package,n);2 将字符串反序列化Request rq;rq.Deserialize(package);3 处理一个请求,并返回结果Response rp = func_(rq);4 将结果序列化string send_string;rp.serialize(&send_string);添加报头发送,响应也要有报头send_string = AddHead(send_string);//发送到网络中write(socket,send_string.c_str(),send_string.size());}}

       添加报头。

        移除报头。

         在第3步的时候我们用func回调了一个函数,这个函数是一开始外部传入的请求处理函数。

func_函数实现。

Response calculate(const Request &rq)
{Response rp(0,0);switch (rq.op_){case '+':rp.result_ = rq.x_ + rq.y_;break;case '-':rp.result_ = rq.x_ - rq.y_;break;case '*':rp.result_ = rq.x_ * rq.y_;break;case '/':if (rq.y_ == 0){rp.exitcode_ = 1;break;}rp.result_ = rq.x_ / rq.y_;break;case '%':if (rq.y_ == 0){rp.exitcode_ = 2;break;}rp.result_ = rq.x_ + rq.y_;break;default:rp.exitcode_ = 3;break;}return rp;
}

四 客户端编写

int main(int argc, char *argv[])
{if (argc != 3){exit(USAGE_ERR);}std::string ip = argv[1];uint16_t port = atoi(argv[2]);Log logs;Sock socks;socks.Socket();socks.Connect(ip, port);logs(ErrorLevel::Debug, "init success: sockt:%d", socks.socket_);std::string inbuffer;while (true){std::cout << "data1# ";Request rq;std::cin >> rq.x_;std::cout << "data2# ";std::cin >> rq.y_;std::cout << "data3# ";std::cin >> rq.op_;// 序列化std::string ret;rq.serialize(&ret);// 添加报头ret = AddHead(ret);// 开始发送send(socks.socket_, ret.c_str(), ret.size(), 0);// 开始读取std::string package;
START:int n = Readpackage(socks.socket_, &inbuffer, &package);if (n < 0){close(socks.socket_);exit(READ_ERR);}else if (n == 0)goto START;// 到了这里就已经读取到了完整的数据报,先去除报头package = RemoveHead(package, n);//将字符串反序列化Response rp;rp.Deserialize(package);    cout<<"result: "<<rp.result_<<endl;cout<<"exitcode: "<<rp.exitcode_<<endl;}return 0;
}

        首先获取到服务端ip和端口。

        创建套接字,并且链接服务器。

                开始获取构建一个请求,这个请求是关于x和y的计算,所以我们首先输入x和y的值,然后把操作数也输入进来,因为这个请求可能是x+y,或者x-y。

        输入完后,要开始准备发送了。当然要先序列化了,把请求转成字符串,但是我们还要添加报头,这个报头是服务端读取一个完整报文的关键。

        此时我们才可以发送,发送完我们还可以接收服务端的响应。

        这个接收服务端的响应的实现就和服务端那边的实现差不多,都是调用Readpackage读取一个完整报文,然后就是去除报头,再反序列化,我们最终可以打印显示响应了,这个响应包括计算结果和错误码。

        由于调用链比较长,我们增加了一些日志,一步步看看我们的序列化和反序列是否符合逻辑。


http://www.ppmy.cn/devtools/2204.html

相关文章

opencv+python(顶帽+黑帽)

1、顶帽运算&#xff1a;去除目标图像外的噪声&#xff0c;原图-开运算&#xff1b; morphologyEx(src, op, # 为形态变换的类型 MORPH_TOPHAT&#xff1a;顶帽&#xff0c;又称礼帽 kernel, dst: , anc…

日常项目管理和开发中经常使用的Git统计命令

日常项目管理和开发中经常使用的Git统计命令 引言应用场景一&#xff1a;统计项目整体提交次数应用场景二&#xff1a;按开发者统计提交数量应用场景三&#xff1a;统计每日/每周提交活动应用场景四&#xff1a;统计单个文件或目录的修改频率应用场景五&#xff1a;按照commitI…

linux应急响应基础命令

一、cpu使用率-top top -c -o %CPU -c 显示进程的命令行参数 -o 按照CPU占用从大到小排序二、用户信息 1、查看系统所有用户信息 [rootcentos7 ~]# cat /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nol…

Qt | 对象树与生命期(对象的创建、销毁、擦查找)

一、组合模式与对象树 1、组合模式指的是把类的对象组织成树形结构,这种树形结构也称为对象树,Qt 使用对象树来管理 QObject 及其子类的对象。注意:这里是指的类的对象而不是类。把类组织成树形结构只需使用简单的继承机制便可实现。 2、使用组合模式的主要作用是可以通过…

NSA发布《在数据支柱中推进零信任成熟度》报告

4月9日&#xff0c;美国国家安全局&#xff08;NSA&#xff09;发布了题为《在数据支柱中推进零信任成熟度》的报告&#xff0c;旨在于数据安全层面提供指导&#xff0c;以增强数据整体安全性并保护静态和传输中的数据。(如下图&#xff09; 一、主要内容 报告中的建议侧重于将…

探索人工智能绘图的奇妙世界

探索人工智能绘图的奇妙世界 人工智能绘图的基本原理机器之美&#xff1a;AI绘图作品AI绘图对艺术创作的影响未来展望与挑战图书推荐&#x1f449;AI绘画教程&#xff1a;Midjourney使用方法与技巧从入门到精通内容简介获取方式&#x1f449;搜索之道&#xff1a;信息素养与终身…

全栈的自我修养 ———— 如何发布一个npm包?

创建本地仓库 npm init在此期间会让你添加一些版本信息和名称 登陆npm npm login ——> yinhaodada arx.040208发布 npm publish查询

数据仓库—ETL技术全景解读:概念、流程与实践

ETL&#xff08;Extract, Transform, Load&#xff09;是数据仓库和数据集成领域的重要概念&#xff0c;用于描述将数据从来源系统抽取、转换和加载到目标系统的过程。本文将介绍ETL的概念、作用和主要过程。 概念 ETL是指将数据从一个系统中抽取出来&#xff08;Extract&…