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

文章目录

  • 一、序列化与反序列化概念
  • 二、自定义协议设计网络计算机
    • 2.1 服务端
      • 2.1.1 服务端业务处理流程
      • 2.1.2 TCP的发送与接收缓冲区
      • 2.1.3 保证读取完整报文
      • 2.1.4 自定义协议——序列化与反序列化
        • 2.1.4.1 请求
        • 2.4.1.2 响应
      • 2.1.5 计算流程
      • 2.1.6 在有效载荷前添加长度报头
      • 2.1.7 发送响应send
      • 2.1.8 读取一个完整的报文recv
    • 2.2 客户端
    • 2.3 结果
  • 三、使用Json进行序列化和反序列化

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

上一章讲解了TCP通信【网络编程】demo版TCP网络服务器实现,我们知道TCP是面向字节流的方式进行通信。
在这里插入图片描述
但是这里就会引发一个问题:怎么保证正好就读到一个完整的数据呢?

举个例子:我们使用QQ发送消息的时候别人接收到的不仅仅只有消息,而是包含了头像信息,昵称,消息。这就叫做结构化的数据。这些结构化的数据可以打包成一个报文(变成一个整体),这个过程就叫做序列化。而把这个整体报文解开的过程就叫做反序列化

结构化数据要先序列化再发送到网络中,收到序列字节流后,要先反序列化再使用。

而这里序列化和反序列化的过程用的就是业务协议

二、自定义协议设计网络计算机

2.1 服务端

自定义协议里要包含两各类,一个是请求,一个是响应
服务端会收到请求,客户端收到响应。

// 请求
class Request
{
public:public:int _x = 0;int _y = 0;char _op = 0;
};// 响应
class Response
{
public:int _exitcode = 0;// 退出码int _result = 0;// 结果
};

请求就是左操作符、右操作符和符号
响应包含了退出码和结果,如果正常结束退出码为0,如果有错误,我们可以自定义不同的退出码表示不同的错误。

2.1.1 服务端业务处理流程

先来看一下服务端处理数据流程

客户端发过来的数据已经序列化成了一个序列字节流数据(报文),所以服务端首先要先把报文反序列化,构成一个结构化请求对象Request。然后就可以进行计算处理形成一个Response对象,再序列化后发送给客户端。

可以看到计算处理这一步其实跟接收发送消息、序列化与反序列化没什么关系,所以可以把计算处理任务在服务端启动的时候传递进去

计算处理函数:
typedef std::function<bool(const Request& req, Response& resp)> func_t;
这里的req是输入型参数(已经反序列化好的对象),resp是输出型参数,为了获取计算结果。

2.1.2 TCP的发送与接收缓冲区

在这里插入图片描述

我们前面使用的write和read接口并不是直接往网络里发送数据或者从网络里读取数据,write其实是把数据拷贝到传输层的缓冲区,由TCP协议决定什么时候把缓冲区的数据发送到网络中。所以TCP协议也叫传输控制协议
发送数据的本质就是将数据从发送缓冲区拷贝到接收缓冲区。

所以客户端/服务端发送数据不会影响接受数据。
所以TCP是全双工的。

而这就会导致一个问题:可能数据堆积在缓冲区来不及度,一次会读取多个报文挨在一起。那么怎么保证读取完整报文呢?

2.1.3 保证读取完整报文

因为TCP是面向字节流的,所以要明确报文与报文的分界。
为什么要这样呢?举个例子:
现在要把两个数字合并成字符串发送,1、12,如果不处理的话就是"112",这样我们反序列化的时候就不知道到底怎么组合了。
而如果我们在分割的地方加一个符号比如,,序列化后:"1,12",这样就很容易拆分。

保证报文读取完整性的方法:
1️⃣ 定长: 规定长度,每次就读取这么多。
2️⃣ 特殊字符: 就是上面的方法。
3️⃣ 自描述方式: 比如在报文前面带上四个字节的字段,标识报文长度。

2.1.4 自定义协议——序列化与反序列化

先来看请求的序列化与反序列化

2.1.4.1 请求

int _x = 0;
int _y = 0;
char _op = 0;

我们希望序列化成这样:"_x _op _y"

#define SEP " "
#define SEP_LEN strlen(SEP)
#define SEP_LINE "\r\n"
#define SEP_LINE_LEN strlen(SEP_LINE)// 请求
class Request
{
public:Request(int x, int y, char op): _x(x), _y(y), _op(op){}Request(){}// 序列化bool serialize(std::string* out/*输出型参数*/){// "_x _op _y"std::string sx = std::to_string(_x);std::string sy = std::to_string(_y);*out = sx + SEP + _op + SEP + sy;return true;}// 反序列化bool deserialize(const std::string& in){// "_x _op _y"auto lsep = in.find(SEP);auto rsep = in.rfind(SEP);if(lsep == std::string::npos || rsep == std::string::npos|| lsep == rsep) return false;std::string sx = in.substr(0, lsep);std::string sy = in.substr(rsep + SEP_LEN);if(sx.empty() || sy.empty()) return false;_x = stoi(sx);_y = stoi(sy);_op = in[lsep + SEP_LEN];return true;}
public:int _x = 0;int _y = 0;char _op = 0;
};

这里的反序列化我们传进去的字符串已经把"\r\n"去掉了。
先来看响应的序列化与反序列化

2.4.1.2 响应

我们希望序列化成这样:"_exitcode _result"

// 响应
class Response
{
public:Response(int exitcode, int result): _exitcode(exitcode), _result(result){}Response(){}// 序列化bool serialize(std::string* out/*输出型参数*/){std::string se = std::to_string(_exitcode);std::string sr = std::to_string(_result);*out = se + SEP + sr;return true;}// 反序列化bool deserialize(const std::string& in){// "_exitcode _result"auto pos = in.find(SEP);if(pos == std::string::npos) return false;std::string se = in.substr(0, pos);std::string sr = in.substr(pos + SEP_LEN);if(se.empty() || sr.empty()) return false;_exitcode = stoi(se);_result = stoi(sr);return true;}
public:int _exitcode = 0;// 退出码int _result = 0;// 结果
};

2.1.5 计算流程

计算结果会形成一个resp响应,里面包含了退出码,我们可以自己设置退出码数值含义:

enum {OK,DIV_ZERO,OP_ERROR
};

计算逻辑:

std::unordered_map<char, std::function<int(int, int)>> hash = 
{{'+', [](int x, int y)->int{return x + y;}},{'-', [](int x, int y)->int{return x - y;}},{'*', [](int x, int y)->int{return x * y;}},{'/', [](int x, int y)->int{return x / y;}},{'%', [](int x, int y)->int{return x % y;}},
};bool calc(const Request& req, Response& resp)
{// req已经反序列化好了if(!hash.count(req._op)) {resp._exitcode = OP_ERROR;return false;}if(req._op == '/' || req._op == '%'){if(req._y == 0){resp._exitcode = DIV_ZERO;return false;}}resp._result = hash[req._op](req._x, req._y);return true;
}

2.1.6 在有效载荷前添加长度报头

在这里插入图片描述

"_x _op _y" -> "content_len\r\n_x _op _y\r\n"
"_exitcode _result" -> "content_len\r\n_exitcode _result\r\n"
// 给有效载荷添加报头信息
std::string enLength(const std::string& text)
{std::string send_str = std::to_string(text.size());send_str += SEP_LINE + text + SEP_LINE;return send_str;
}  

2.1.7 发送响应send

#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);RETURN VALUE
On success, these calls return the number of characters sent.  
On error, -1 is returned, and errno is set appropriately.

服务端收到请求到把响应发送出去的整个流程:

// 处理请求的入口
void handler(int sock, func_t func)
{// 得到序列化好的请求对象std::string req_str;// 得到结构化请求对象Request req;if(!req.deserialize(req_str)) return;// 计算,得到响应Response resp;func(req, resp);// 序列化响应std::string resp_str;resp.serialize(&resp_str);// 添加报头std::string send_str = enLength(resp_str);// 发送响应send(sock, send_str.c_str(), send_str.size(), 0);
}

那么这里的第一步是怎么读取请求的呢?
这个请求必须是恰好一个完整的请求。

2.1.8 读取一个完整的报文recv

#include <sys/types.h>
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);RETURN VALUE
These  calls return the number of bytes received, or -1 if an error occurred.  In the event of an error, errno is set to indicate the error.  The
return value will be 0 when the peer has performed an orderly shutdown.
// "content_len\r\n_x _op _y\r\n"
// 读取一个完整报文
bool recvPackage(int sock, std::string& inbuf, std::string* out)
{char buf[1024];while(true){ssize_t n = recv(sock, buf, sizeof buf - 1, 0);if(n > 0){buf[n] = '\0';inbuf += buf;auto pos = inbuf.find(SEP_LINE);if(pos == std::string::npos) continue;// 还得继续读取std::string text_len = inbuf.substr(0, pos);int content_len = stoi(text_len);int total_len = text_len.size() + 2 * SEP_LINE_LEN + content_len;// 一个完整报文长度if(inbuf.size() < total_len) continue;// 还得继续读取// 至少有一个完整报文*out = inbuf.substr(0, total_len);inbuf.erase(0, total_len);return true;}else return false;}return true;
}

收到的请求还需要去掉报头

// 去掉有效载荷的报头信息
bool deLength(const std::string& pack, std::string *out)
{auto pos = pack.find(SEP_LINE);if(pos == std::string::npos) return false;std::string text_len_string = pack.substr(0, pos);int text_len = stoi(text_len_string);*out = pack.substr(pos + SEP_LINE_LEN, text_len);return true;
}

这样服务端的业务逻辑就完成了:

// 处理请求的入口
void handler(int sock, func_t func)
{std::string inbuf;// 输入缓冲区while(1){// 得到序列化好的请求对象std::string req_text;if(!recvPackage(sock, inbuf, &req_text)) return;std::string req_str;if(!deLength(req_text, &req_str)) return;// 得到结构化请求对象Request req;if(!req.deserialize(req_str)) return;// 计算,得到响应Response resp;func(req, resp);// 序列化响应std::string resp_str;resp.serialize(&resp_str);// 添加报头std::string send_str = enLength(resp_str);// 发送响应send(sock, send_str.c_str(), send_str.size(), 0);}
}

2.2 客户端

大致流程跟服务端差不多:

void start()
{struct sockaddr_in si;bzero(&si, sizeof si);si.sin_family = AF_INET;si.sin_port = htons(_serverport);si.sin_addr.s_addr = inet_addr(_serverip.c_str());if(connect(_sock, (struct sockaddr*)&si, sizeof si) < 0){std::cout << "connect socket error" << std::endl;}else{std::string msg;std::string inbuf;// 输入缓冲区while(1){std::cout << "Please Enter#";std::getline(std::cin, msg);// 1+2// 解析字符串Request req = PraseMsg(msg);// 序列化std::string content;req.serialize(&content);// 添加报头std::string send_str = enLength(content);// 发送send(_sock, send_str.c_str(), send_str.size(), 0);// 获取响应结果std::string package;// "content_len\r\n_x _op _y\r\n"if(!recvPackage(_sock, inbuf, &package)) continue;// 还要继续读// 去掉报头,提取正文std::string text;if(!deLength(package, &text)) continue;// 反序列化获取退出码和结果Response resp;resp.deserialize(text);std::cout << "exitcode: " << resp._exitcode << std::endl;std::cout << "result: " << resp._result << std::endl;}}
}// 解析字符串
Request PraseMsg(const std::string& msg)
{// "123+456"int idx_op = 0;int idx = 0, n = msg.size();// 找符号位置while(idx < n){if(hash.count(msg[idx])){idx_op = idx;break;}idx++;}Request req;std::string sx = msg.substr(0, idx_op);std::string sy = msg.substr(idx_op + 1);req._x = stoi(sx);req._y = stoi(sy);req._op = msg[idx_op];return req;
}

流程就是序列化请求,添加报头,发送,接收响应,去掉报头,反序列化,获取结果。

2.3 结果

客户端:
在这里插入图片描述
服务端:
在这里插入图片描述

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

序列化与反序列化其实C++提供了Json的库。我们可以直接使用:

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

首先先安装Json库。

sudo yum install -y jsoncpp-devel

头文件:#include <jsoncpp/json/json.h>

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

Makefile:

.PHONY:all
all:CalcServer CalcClientCalcClient:CalcClient.ccg++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
CalcServer:CalcServer.ccg++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF.PHONY:clean
clean:rm -f CalcClient CalcServer
// 请求
class Request
{
public:Request(int x, int y, char op): _x(x), _y(y), _op(op){}Request(){}// 序列化bool serialize(std::string* out/*输出型参数*/){
#ifdef MYSELF// "_x _op _y"std::string sx = std::to_string(_x);std::string sy = std::to_string(_y);*out = sx + SEP + _op + SEP + sy;
#elseJson::Value root;// 万能对象,可接收任何对象root["first"] = _x;// 自动将_x转换为字符串root["second"] = _y;root["oper"] = _op;// 序列化Json::FastWriter writer;*out = writer.write(root);// 将root进行序列化
#endifreturn true;}// 反序列化bool deserialize(const std::string& in){
#ifdef MYSELF// "_x _op _y"auto lsep = in.find(SEP);auto rsep = in.rfind(SEP);if(lsep == std::string::npos || rsep == std::string::npos|| lsep == rsep) return false;std::string sx = in.substr(0, lsep);std::string sy = in.substr(rsep + SEP_LEN);if(sx.empty() || sy.empty()) return false;_x = stoi(sx);_y = stoi(sy);_op = in[lsep + SEP_LEN];
#else//Json反序列化Json::Value root;// 万能对象,可接收任何对象Json::Reader reader;reader.parse(in,root);// 第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中//反序列化_x = root["first"].asInt();// 默认是字符串,转换为整型_y = root["second"].asInt();_op = root["oper"].asInt();// 转换为整型,整型可以给char类型
#endifreturn true;}
public:int _x = 0;int _y = 0;char _op = 0;
};// 响应
class Response
{
public:Response(int exitcode, int result): _exitcode(exitcode), _result(result){}Response(){}// 序列化bool serialize(std::string* out/*输出型参数*/){
#ifdef MYSELFstd::string se = std::to_string(_exitcode);std::string sr = std::to_string(_result);*out = se + SEP + sr;
#elseJson::Value root;// 万能对象,可接收任何对象root["exitcode"] = _exitcode;// 自动将_exit转换为字符串root["result"] = _result;// 序列化Json::FastWriter writer;*out = writer.write(root);// 将root进行序列化
#endifreturn true;}// 反序列化bool deserialize(const std::string& in){// "_exitcode _result"
#ifdef MYSELFauto pos = in.find(SEP);if(pos == std::string::npos) return false;std::string se = in.substr(0, pos);std::string sr = in.substr(pos + SEP_LEN);if(se.empty() || sr.empty()) return false;_exitcode = stoi(se);_result = stoi(sr);
#else//Json反序列化Json::Value root;// 万能对象,可接收任何对象Json::Reader reader;reader.parse(in,root);// 第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中//反序列化_exitcode = root["exitcode"].asInt();// 默认是字符串,转换为整型_result = root["result"].asInt();
#endifreturn true;}
public:int _exitcode = 0;// 退出码int _result = 0;// 结果
};


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/4334.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

威胁建模之绘制数据流图

0x00 前言 1、什么是威胁建模&#xff1a; 以结构化的方式思考、记录并讨论系统存在的安全威胁&#xff0c;并针对这些威胁制定相应的消减措施。 2、为什么要威胁建模&#xff1a; &#xff08;1&#xff09;在设计阶段开展威胁建模&#xff0c;一方面可以更全面的发现系统存…

处理glibc堆栈缓冲区溢出漏洞(CVE-2018-11236)

GNU C Library&#xff08;又名glibc&#xff0c;libc6&#xff09;是一种按照LGPL许可协议发布的开源免费的C语言编译程序。 GNU C库&#xff08;aka glibc或libc6&#xff09;中的stdlib/canonicalize.c处理非常长的路径名参数到realpath函数时&#xff0c;可能会遇到32位体系…

Electron中启动node服务

记一次遇到的问题&#xff0c;我们知道Electron 中主进程是在node环境中&#xff0c;所以打算在node环境中再启动一个node服务。但是直接使用exec命令启动就会卡主。对应的代码如下 // 启动Node server const startServer async () > {try {console.log(开始启动node serv…

Docker学习笔记11

Docker容器镜像&#xff1a; 1&#xff09;docker client 向docker daemon发起创建容器的请求&#xff1b; 2&#xff09;docker daemon查找本地有客户端需要的镜像&#xff1b; 3&#xff09;如无&#xff0c;docker daemon则到容器的镜像仓库中下载客户端需要的镜像&#…

CSS基础

文章目录 前言CSS基本语法CSS选择器CSS基本选择器标签选择器 p类选择器 .pID选择器 #p CSS后代选择器 div pCSS子选择器 div>pCSS群组选择器 p,p1CSS伪类选择器:first-of-type 父类第一个:last-of-type 父类最后一个:nth-of-type(n) 父类第n个 CSS使用方式行内样式内嵌样式外…

poi生成excel饼图设置颜色

效果 实现 import com.gideon.entity.ChartPosition; import com.gideon.entity.LineChart; import com.gideon.entity.PieChart; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xddf.usermodel.PresetColo…

深度学习(23)——YOLO系列(2)

深度学习&#xff08;23&#xff09;——YOLO系列&#xff08;2&#xff09; 文章目录 深度学习&#xff08;23&#xff09;——YOLO系列&#xff08;2&#xff09;1. model2. dataset3. utils4. test/detect5. detect全过程 今天先写YOLO v3的代码&#xff0c;后面再出v5&…

电脑出现0xC1900101错误怎么办?

在更新或安装Windows操作系统时&#xff0c;有时系统会提示出现了0xC1900101错误。这个错误的出现通常是源于与驱动程序相关的错误所致。那么当电脑出现0xC1900101错误时该怎么办呢&#xff1f; 为什么会出现错误代码0xC1900101&#xff1f; 通常情况下&#xff0c;有以下几个…

使用Python批量进行数据分析

案例01 批量升序排序一个工作簿中的所有工作表——产品销售统计表.xlsx import xlwings as xw import pandas as pd app xw.App(visible False, add_book False) workbook app.books.open(产品销售统计表.xlsx) worksheet workbook.sheets # 列出工作簿中的所有工作表 fo…

通用分页【下】(将分页封装成标签)

目录 一、debug调试 1、什么是debug调试&#xff1f; 2、debug调试步骤 3、实践 二、分页的核心 三、优化 分页工具类 编写servlet jsp代码页面&#xff1a; 分页工具类PageBean完整代码 四、分页标签 jsp代码 编写标签 tld文件 助手类 改写servlet 解析&…

使用el-menu做侧边栏导航遇到需要点击两次菜单才展开

在根据路由遍历生成侧边导航栏时&#xff0c;遇到一个问题&#xff0c;就是当我点击选中某个垂直菜单时&#xff0c;只有点击第二次它才会展开&#xff0c;第一次在选中垂直菜单之后垂直菜单它就收缩起来了&#xff0c;如下图&#xff1a; 如上图&#xff0c;在我第一次点击选…

带纵深可跳跃横版闯关游戏模版

此项目是以《卡比猎人队》为蓝本开发的横版带纵深闯关游戏模版。内涵数据表配置文件。 购买链接&#xff1a; 微店购买链接 开发环境 开发引擎&#xff1a;CocosCreator3.6.3开发语言&#xff1a;TypeScript 包含的内容&#xff1a; 逻辑实现目录介绍&#xff08;game&am…