【网络编程】协议定制+Json序列化与反序列化

news/2024/10/18 2:34:41/


目录

一、序列化与反序列化的概念

二、自定义协议设计一个网络计算器

2.1TCP协议,如何保证接收方收到了完整的报文呢?

2.2自定义协议的实现

2.3自定义协议在客户端与服务器中的实现

三、使用Json进行序列化和反序列化

3.1jsoncpp库的安装

3.2改造自定义协议

3.3自定义协议的命名区分


网络版计算器代码可参考博主gitee。

一、序列化与反序列化的概念

        序列化是指将对象转换为字节流或其他可存储或传输的格式,以便将其存储在文件中或通过网络发送到另一个系统。反序列化是指将序列化的数据重新转换为对象。在序列化和反序列化过程中,对象的状态信息被保存和恢复,以保证数据的完整性和正确性。在分布式系统中,序列化和反序列化是实现远程方法调用和消息传递的重要技术。

        例如微信发送一条消息,会将头像、昵称、消息内容、发送时间等“结构化”数据进行序列化,形成一个字节流报文,通过网络将该报文发送给接收方,接收方进行反序列化将报文重新拆解为头像、昵称、消息内容、发送时间等“结构化”数据。

二、自定义协议设计一个网络计算器

2.1TCP协议,如何保证接收方收到了完整的报文呢?

1、我们调用的所有发送/接收函数,并不是直接从网络中发送/接收数据,应用层调用的发送/接收函数,本质是一个拷贝函数。
例如客户端发送数据时,应用层调用发送函数将会把应用层的发送缓冲区数据拷贝至传输层的发送缓冲区。传输层自主决定何时将发送缓冲区的数据发送至网络里,再通过网络发送至服务器的接收缓冲区中,所以TCP协议是一种传输控制协议。

2、TCP协议的通信双方的发送缓冲区和接收缓冲区互不干扰,可以双向同时进行通信。TCP是一种全双工的通信协议。

3、如果TCP服务器的读取速度跟不上客户端的发送速度,将会导致服务器接收缓冲区积攒大量的报文,这些报文数据可是一连串的粘连在一起的,如何一条一条的将完整的报文提取出来呢?使用协议!协议设计方式:

  • 定长(例如规定该报文定长为1024字节)
  • 特殊符号(在报文和报文之间增加特殊符号)
  • 自描述方式(自己设计协议)

本文代码协议设计如下图所示:使用该协议设计一个网络计算器。

        如果是UDP协议,UDP客户端,发送报文时只需创建请求,对请求进行序列化后即可发送;接收报文时只需将接收的数据进行反序列化即可。无需进行协议内容的添加与解析。这是因为UDP每次发送与接收都是以数据报的形式,数据是完整的,不像TCP是面向字节流,需要使用相关的协议进行界定报文边界。

2.2自定义协议的实现

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
enum
{OK=0,DIV_ZERO_ERR,MOD_ZERO_ERR,OP_ZERO_ERR,
};
#define SEP " "
#define SEP_LEN strlen(SEP)//不能使用sizeof,用sizeof会统计到'\0'
#define LINE_SEP "\r\n"
#define LINE_SEP_LINE strlen(LINE_SEP)
//"_exitcode result" -> "content_len"\r\n"_exitcode result"\r\n
//"_x _op _y" -> "content_len"\r\n"_x _op _y"\r\n
std::string enLength(const std::string& text)//text:_x _op _y。添加协议规则,用于构建一个完整的报文(类似"打包")
{std::string send_string=std::to_string(text.size());//计算有效载荷的长度"_x _op _y"send_string+=LINE_SEP;send_string+=text;send_string+=LINE_SEP;return send_string;
}
//_exitcode result
bool deLength(const std::string& package,std::string* text)//获取报文中的有效载荷(类似"解包")
{auto pos=package.find(LINE_SEP);if(pos==std::string::npos){return false;}int textLen=std::stoi(package.substr(0,pos));//计算有效载荷的长度*text=package.substr(pos+LINE_SEP_LINE,textLen);return true;
}
class Request//请求类
{
public:Request(int x,int y,char op):_x(x),_y(y),_op(op){}Request():_x(0),_y(0),_op(0){}bool serialize(std::string* out)//序列化,将成员变量转字符串{//结构化->"_x _op _y"*out="";//清空string对象std::string x_tostring=std::to_string(_x);std::string y_tostring=std::to_string(_y);*out=x_tostring+SEP+_op+SEP+y_tostring;//_x _op _yreturn true;}bool deserialize(const std::string& in)//反序列化{//"_x _op _y"->结构化auto leftSpace=in.find(SEP);//左边的空格auto rightSpace=in.rfind(SEP);//右边的空格if(leftSpace==std::string::npos||rightSpace==std::string::npos){return false;}if(leftSpace==rightSpace){return false;} //子串提取std::string x_tostring=in.substr(0,leftSpace);if(rightSpace-(leftSpace+SEP_LEN)!=1){return false;}//表示操作符一定只占1位_op=in.substr(leftSpace+SEP_LEN,rightSpace-(leftSpace+SEP_LEN))[0];std::string y_tostring=in.substr(rightSpace+SEP_LEN);//对x,y进行转换_x=std::stoi(x_tostring); _y=std::stoi(y_tostring);return true;}
public://_x _op _yint _x;//左操作数int _y;//右操作数char _op;//操作符
};class Response//响应类
{
public:Response():_exitCode(0),_result(0){}Response(int exitCode,int result):_exitCode(exitCode),_result(result){}bool serialize(std::string* out)//序列化,将成员变量转string对象{*out="";//清空string对象std::string outString=std::to_string(_exitCode)+SEP+std::to_string(_result);*out=outString;return true; }bool deserialize(const std::string& in)//反序列化{auto space=in.find(SEP);//找空格if(space==std::string::npos){return false;}std::string exitString=in.substr(0,space);std::string resString=in.substr(space+SEP_LEN);if(exitString.empty()||resString.empty()){return false;}//一个字符串为空就false_exitCode=std::stoi(exitString);_result=std::stoi(resString);return true;}
public:int _exitCode;//0表示计算成功,非零代表除零等错误int _result;//运算结果
};bool recvPackage(int sock,std::string& inbuffer,std::string* text)//服务器/客户端读取报文
{//将缓冲区数据拆分成一个个报文"content_len"\r\n"_x _op _y"\r\nchar buffer[1024];while(1){ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);//阻塞式读取,等价于read接口if(n>0){buffer[n]=0;//字符串末尾添加'\0'inbuffer+=buffer;//拆分成一个个报文auto pos=inbuffer.find(LINE_SEP);//找\r\n的起始位置if(pos==std::string::npos)//没找到说明暂时还没找到\r\n分隔符,跳过本次循环,等待下次读取{continue;}std::string textLenString=inbuffer.substr(0,pos);int textLen=std::stoi(textLenString);//拿出有效载荷的长度int totalLen=textLenString.size()+2*LINE_SEP_LINE+textLen;//单个报文总长度if(inbuffer.size()<totalLen)//说明缓冲区长度还不到一个报文大小,需要跳过本次循环继续读取{continue;}std::cout<<"截取报文前inbuffer中的内容:\n"<<inbuffer<<std::endl;//走到这里,一定有一个完整的报文*text=inbuffer.substr(0,totalLen);//取出一个报文inbuffer.erase(0,totalLen);//删掉缓冲区中刚刚被提取走的报文数据std::cout<<"截取报文后inbuffer中的内容:\n"<<inbuffer<<std::endl;break;}else{return false;}}return true;
}

代码解释:

本协议设计了一个Request请求类和一个Response响应类,请求类存储计算器的操作数和操作符,响应类存储计算结果和退出码。这两个类中各自实现了对有效载荷的序列化与反序列化的接口。

enLength用于给有效载荷添加报头,即自定义协议的规则。deLength则用于解析收到的报文,剔除报文中的协议报头,提取出其中的有效载荷。协议是明确缓冲区中报文与报文之间边界的一种特殊格式,而enLength和deLength用于添加报头和去掉报头

2.3自定义协议在客户端与服务器中的实现

三、使用Json进行序列化和反序列化

3.1jsoncpp库的安装

        从上方代码可以看到,使用string对象手动进行序列化与反序列化非常麻烦。可以使用Json进行序列化与反序列化操作。

        Json(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于Web应用程序中的数据传输。它是一种基于文本的格式,易于读写和解析。Json格式的数据可以被多种编程语言支持,包括JavaScript、Python、Java、C#、C++等。Json数据由键值对组成,使用大括号表示对象,使用方括号表示数组。

        C++使用Json需要包含Jsoncpp库,yum安装Jsoncpp库指令:先执行第二句,如果报错再执行第一句!

sudo mv /var/lib/rpm/__db.00* /tmp/&&yum clean all
sudo yum install -y jsoncpp-devel

3.2改造自定义协议

makefile:使用jsoncpp库记得在编译时加上-ljsoncpp

cc=g++#将cc变量设置为g++编译器
LD=#-DMYSELF
.PHONY:all
all:calClient calServercalClient:calClient.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}
calServer:calServer.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}.PHONY:clean
clean:rm -f calClient calServer

改造的协议: 

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <jsoncpp/json/json.h>
#include <sys/types.h>
#include <sys/socket.h>
enum
{OK=0,DIV_ZERO_ERR,MOD_ZERO_ERR,OP_ZERO_ERR,
};
#define SEP " "
#define SEP_LEN strlen(SEP)//不能使用sizeof,用sizeof会统计到'\0'
#define LINE_SEP "\r\n"
#define LINE_SEP_LINE strlen(LINE_SEP)
//"_exitcode result" -> "content_len"\r\n"_exitcode result"\r\n
//"_x _op _y" -> "content_len"\r\n"_x _op _y"\r\n
std::string enLength(const std::string& text)//text:_x _op _y。添加协议规则,用于构建一个完整的报文(类似"打包")
{std::string send_string=std::to_string(text.size());//计算有效载荷的长度"_x _op _y"send_string+=LINE_SEP;send_string+=text;send_string+=LINE_SEP;return send_string;
}
//_exitcode result
bool deLength(const std::string& package,std::string* text)//获取报文中的有效载荷(类似"解包")
{auto pos=package.find(LINE_SEP);if(pos==std::string::npos){return false;}int textLen=std::stoi(package.substr(0,pos));//计算有效载荷的长度*text=package.substr(pos+LINE_SEP_LINE,textLen);return true;
}
class Request//请求类
{
public:Request(int x,int y,char op):_x(x),_y(y),_op(op){}Request():_x(0),_y(0),_op(0){}bool serialize(std::string* out)//序列化,将成员变量转字符串{
#ifdef MYSELF//结构化->"_x _op _y"*out="";//清空string对象std::string x_tostring=std::to_string(_x);std::string y_tostring=std::to_string(_y);*out=x_tostring+SEP+_op+SEP+y_tostring;//_x _op _y
#else//Json序列化Json::Value root;//Json::Value万能对象,可接收任何对象root["first"]=_x;//自动将_x转换为字符串root["second"]=_y;root["oper"]=_op;//序列化Json::FastWriter writer;//Json::StyledWriter write;等价*out=writer.write(root);//将root进行序列化,返回值为string对象,接收即可
#endifreturn true;}bool deserialize(const std::string& in)//反序列化{
#ifdef MYSELF//"_x _op _y"->结构化auto leftSpace=in.find(SEP);//左边的空格auto rightSpace=in.rfind(SEP);//右边的空格if(leftSpace==std::string::npos||rightSpace==std::string::npos){return false;}if(leftSpace==rightSpace){return false;} //子串提取std::string x_tostring=in.substr(0,leftSpace);if(rightSpace-(leftSpace+SEP_LEN)!=1){return false;}//表示操作符一定只占1位_op=in.substr(leftSpace+SEP_LEN,rightSpace-(leftSpace+SEP_LEN))[0];std::string y_tostring=in.substr(rightSpace+SEP_LEN);//对x,y进行转换_x=std::stoi(x_tostring); _y=std::stoi(y_tostring);
#else//Json反序列化Json::Value root;//Json::Value万能对象,可接收任何对象Json::Reader reader;reader.parse(in,root);//第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中//反序列化_x=root["first"].asInt();//默认是字符串,转换为整型_y=root["second"].asInt();_op=root["oper"].asInt();//转换为整型,整型可以给char类型。
#endifreturn true;}
public://_x _op _yint _x;//左操作数int _y;//右操作数char _op;//操作符
};class Response//响应类
{
public:Response():_exitCode(0),_result(0){}Response(int exitCode,int result):_exitCode(exitCode),_result(result){}bool serialize(std::string* out)//序列化,将成员变量转string对象{
#ifdef MYSELF*out="";//清空string对象std::string outString=std::to_string(_exitCode)+SEP+std::to_string(_result);*out=outString;
#else//Json序列化Json::Value root;//Json::Value万能对象,可接收任何对象root["exitCode"]=_exitCode;//自动将_exitCode转换为字符串root["result"]=_result;//序列化Json::FastWriter writer;//Json::StyledWriter write;等价*out=writer.write(root);//将root进行序列化,返回值为string对象,接收即可
#endifreturn true; }bool deserialize(const std::string& in)//反序列化{
#ifdef MYSELFauto space=in.find(SEP);//找空格if(space==std::string::npos){return false;}std::string exitString=in.substr(0,space);std::string resString=in.substr(space+SEP_LEN);if(exitString.empty()||resString.empty()){return false;}//一个字符串为空就false_exitCode=std::stoi(exitString);_result=std::stoi(resString);
#else//Json反序列化Json::Value root;//Json::Value万能对象,可接收任何对象Json::Reader reader;reader.parse(in,root);//第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中//反序列化_exitCode=root["exitCode"].asInt();//默认是字符串,转换为整型_result=root["result"].asInt();
#endifreturn true;}
public:int _exitCode;//0表示计算成功,非零代表除零等错误int _result;//运算结果
};bool recvPackage(int sock,std::string& inbuffer,std::string* text)//服务器/客户端读取报文
{//将缓冲区数据拆分成一个个报文"content_len"\r\n"_x _op _y"\r\nchar buffer[1024];while(1){ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);//阻塞式读取,等价于read接口if(n>0){buffer[n]=0;//字符串末尾添加'\0'inbuffer+=buffer;//拆分成一个个报文auto pos=inbuffer.find(LINE_SEP);//找\r\n的起始位置if(pos==std::string::npos)//没找到说明暂时还没找到\r\n分隔符,跳过本次循环,等待下次读取{continue;}std::string textLenString=inbuffer.substr(0,pos);int textLen=std::stoi(textLenString);//拿出有效载荷的长度int totalLen=textLenString.size()+2*LINE_SEP_LINE+textLen;//单个报文总长度if(inbuffer.size()<totalLen)//说明缓冲区长度还不到一个报文大小,需要跳过本次循环继续读取{continue;}std::cout<<"截取报文前inbuffer中的内容:\n"<<inbuffer<<std::endl;//走到这里,一定有一个完整的报文*text=inbuffer.substr(0,totalLen);//取出一个报文inbuffer.erase(0,totalLen);//删掉缓冲区中刚刚被提取走的报文数据std::cout<<"截取报文后inbuffer中的内容:\n"<<inbuffer<<std::endl;break;}else{return false;}}return true;
}
bool recvPackageAll(int sock,std::string& inbuffer,std::vector<std::string>* out)
{std::string line; while(recvPackage(sock,inbuffer,&line)){out->push_back(line);}
}

3.3自定义协议的命名区分

        未来在一套系统中可以自定义多种协议,为了区分不同的自定义协议,可以参照如下格式设计协议的格式:

当然前人已经设计好了常见的网络协议,例如http/https。


http://www.ppmy.cn/news/80320.html

相关文章

如何用c++制作人生模拟器

要制作一个人生模拟器&#xff0c;首先需要设计游戏的基本框架&#xff0c;并构思游戏的玩法&#xff0c;规则和内容。 然后&#xff0c;在C中实现这个框架并添加游戏所需的各种类、函数和变量。其中&#xff0c;有几个关键的方面需要考虑&#xff1a; 模拟生命周期&#xff…

BigDecimal类型的数据如何保留小数点后四位

BigDecimal类型的数据如何保留小数点后四位 下面是使用Java的BigDecimal类来保留小数点后四位的示例&#xff1a; import java.math.BigDecimal; import java.math.RoundingMode;public class Main {public static void main(String[] args) {BigDecimal number new BigDecima…

日志模块封封装:单例模式+策略模式+构建者模式+bugly

日志模块封装:单例模式策略模式构建者模式bugly 一.单例模式策略模式构建者模式二.日志模块封装1.日志等级&#xff1a;LoggerLevel枚举类2.日志输出策略&#xff1a;LoggerStrategy枚举类3.ILogger接口4.LogCatLogger/FileLogger/NetWorkLogger/EmailLogger5.使用构建者模式和…

RocketMq源码分析(七)--消息发送流程

文章目录 一、消息发送入口二、消息发送流程1、消息验证1&#xff09;消息主题验证2&#xff09;消息内容验证 2、查找路由3、消息发送1&#xff09;选择消息队列2&#xff09;消息发送-内核实现sendKernelImpl方法参数获取brokerAddr添加消息全局唯一id设置实例id设置系统标记…

Rust每日一练(Leetday0010) 子串下标、两数相除、串联子串

目录 28. 找出字符串中第一个匹配项的下标 Find-the-index-of-the-first-occurrence-in-a-string &#x1f31f;&#x1f31f; 29. 两数相除 Divide Two Integers &#x1f31f;&#x1f31f; 30. 串联所有单词的子串 Substring-with-concatenation-of-all-words &#x…

Java常见Exception

运行时异常和非运行时异常 运行时异常&#xff1a;都是RuntimeException类及其子类异常&#xff1a; IndexOutOfBoundsException 索引越界异常ArithmeticException:数学计算异常NullPointerException:空指针异常ArrayOutOfBoundsException:数组索引越界异常ClassNotFoundExce…

Linux网络编程—Day10

Linux服务器程序规范 Linux服务器程序一般以后台进程形式运行。后台进程又称守护进程。它没有控制终端&#xff0c;因而也不会意外接收到用户输入。 守护进程的父进程通常是init进程&#xff08;PID为1的进程&#xff09;&#xff1b;Linux服务器程序通常有一套日志系统&#…

黑客如何从零学起?

一、MYSQL5.7 MySQL是如今使用最多的数据库&#xff0c;是众多企业的首选&#xff0c;在未来几年都将被持续推动发展。 学习MySQL需注重实战操作&#xff0c;循序渐进地了解MySQL中的各项技术&#xff0c;这样才能在实际工作中的关键应用。 想进入网络安全行业&#xff0c; …