【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构

在这里插入图片描述

唯有梦想才配让你不安,
唯有行动才能解除你的不安。
--- 卢思浩 ---

从零开始掌握序列化

  • 1 知识回顾
  • 2 序列化与编写协议
    • 2.1 使用Json进行序列化
    • 2.2 编写协议
  • 3 封装IOService
  • 4 应用层 --- 网络计算器
  • 5 总结

1 知识回顾

上一篇文章我们讲解了协议的本质是双方能够看到的结构化数据。并通过传输层的底层理解了为什么read系列函数时全双工支持同时读写的:TCP传输层有两个缓冲区,分别接收和发送。最重要的是我们将TCP通信的代码进行的重构:

  1. 我们将Socket通信单独封装为一个类,负责Socket套接字的创建,bind绑定服务器端口号,进入监听模式…工作,基类Socket并不进行定义,只进行声明!具体实现由派生类TcpServer和UdpServer来进行
  2. TcpServer继承Socket类的所有方法,然后进行具体的函数定义!
  3. 上层的TcpServer直接底层使用TcpSocket对象就可以完成Socket系列操作,十分方便!

接下来我们要实现是这样的一个结构:
在这里插入图片描述
通信过程整体分为三层

  1. 传输层TcpServer:负责从Socket文件中获取链接,传输层不需要进行IO,获取到连接就让会话层通过连接获取数据!
  2. 会话层Service:根据传输层给的连接,从Sockfd文件中读取数据,解析出报文结构中的数据字符串,然后通过协议分离出结构化数据。该层只负责数据的解析,数据的处理交给应用层进行!
  3. 应用层Process:应用层是具有的业务逻辑,根据会话层解析出的数据,进行数据处理!

这样是一个非常非常优雅的封装操作!!!

2 序列化与编写协议

2.1 使用Json进行序列化

协议是IO的基础,只有协议确定下来,才可以进行通信。
我们这里想要实现一个网络计算器的应用,所以协议分为了两个类:Request和Response。分别作为传入的数据和传出的数据:

  1. Request:两个数字和一个运算符
  2. Response:结果数字 , 错误码 ,退出信息

他们是作为结构化的数据进行传输,那么想要进行传输就来到了最重要的部分序列化与反序列化!序列化与反序列化可以使用第三方库也可以自己进行编写。这里我们先使用第三方的Json库进行实现:

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

  1. 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。
  2. 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。
  3. 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数字、 布尔值和 null。
  4. 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便开发者调试

在Linux中使用需要进行安装对应的JSON库:

ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

安装之后就可以进行使用了:

使用起来是十分方便的:

  1. Json::Value是最重要的类,这是对Json数据结构进程操作和表示的关键类
  2. 建立好类Json::Value之后就可以通过[ ]操作root["x"] = _x;,像这样就可以进行赋值
  3. 将Json数据结构转换为字符串依靠 Json::FastWriter 或 Json::StreamWriter都可以转换成字符串
Json::StyledWriter writer;
std::string s = writer.write(root)
  1. 通过Json::Reader可以快速将字符串反序列化得到Json结构!
bool parsingSuccessful = reader.parse(json_string,root);
// 访问 JSON 数据 
std::string name = root["name"].asString();
int age = root["age"].asInt(); std::string city =
root["city"].asString();

通过这样就就可以简洁的完成序列化与反序列化的工作!

2.2 编写协议

根据我们的需求在加入Json操作我们就可以把协议写出来,代码虽然很长但是很好理解:

  1. Request类中需要根据 int x , int y , char oper进行序列化生成字符串,也要能够通过字符串反序列化得到三个变量
  2. Response类中需要根据 int res , int code , std::string desc进行序列化生成字符串,也要能够通过字符串反序列化得到三个变量
#pragma once
#include <jsoncpp/json/json.h>
#include <string>
// 协议就是双方都认识的结构化数据
// "len"\r\n"{json}"\r\n
const std::string sep = "\r\n";struct Request
{
public:Request() {}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}~Request(){}bool Serialize(std::string *out){// 使用现成的 Json 库Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(std::string &in){Json::Value root;    // 创建json对象Json::Reader reader; // 读取bool res = reader.parse(in, root);if (res == false)return false;_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}int X() { return _x; }int Y() { return _y; }char Oper() { return _oper; }private:int _x;int _y;char _oper;
};struct Response
{Response() {}Response(int res, int code, std::string desc) : _res(res), _code(code), _desc(desc){}~Response(){}bool Serialize(std::string *out){// 使用现成的 Json 库Json::Value root;root["res"] = _res;root["code"] = _code;root["desc"] = _desc;Json::FastWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(std::string &in){Json::Value root;    // 创建json对象Json::Reader reader; // 读取bool res = reader.parse(in, root);if (res == false)return false;_res = root["res"].asInt();_code = root["code"].asInt();_desc = root["desc"].asInt();return true;}int _res;int _code; // 退出码 0:success 1:div zero 2:非法操作std::string _desc;
};

看一下效果:
在这里插入图片描述

完成了基础的序列化和反序列化之后,我们就可以做到从sockfd流中读取数据了吗??不可以!因为不知道Json字符串的长度,就不知道应该读取多少字节!这样可就做不到正确的从数据中获取json字符串!

所以我们还有做一步特殊处理:

  • 需要对生成的Json字符串加入报头len记录json字符串的长度,中间以sep分隔符分割!
  • 需要对获得到的数据进行解析,去除报头得到一个Json字符串!
// "len"\r\n"{json}"\r\n
const std::string sep = "\r\n";
// 加入报头
std::string Encode(const std::string &jsonstr)
{int len = jsonstr.size();std::string lenstr = std::to_string(len);return lenstr + sep + jsonstr + sep;
}std::string Decode(std::string &packagestream)
{auto pos = packagestream.find(sep);if (pos == std::string::npos)return std::string();// 获取到lenstd::string lenstr = packagestream.substr(0, pos);int len = std::stoi(lenstr);//算上报头的完整长度!int total = lenstr.size() + len + 2 * sep.size();if (total > packagestream.size())return std::string();// 到这里说明可以读取完整数据std::string jsonstr = packagestream.substr(pos + sep.size(), len);packagestream.erase(total);return jsonstr;
}

经过这样的操作,可以保证:

  • 上层想要发送数据时,可以将数据包装为json字符串,并加入报头形成完整报文!
  • 上层获取数据进行反序列化时可以获取到完整的json字符串!并成功解析为数据

3 封装IOService

将来我们的线程会执行将会执行这个回调函数方法,现在我们不再需要TcpServer来进行IO操作,TcpServer只负责进行获取链接,获取到连接后通过ThreadData结构体将数据传到线程中的回调函数中:

	class ThreadData{public:SockSPtr _sockfd;InetAddr _addr;TcpServer *_this;public:ThreadData(SockSPtr sockfd, InetAddr addr, TcpServer *p) : _sockfd(sockfd),_this(p),_addr(addr){}};

在回调函数Execute中:

// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!static void *Execute(void *args){pthread_detach(pthread_self()); // 线程分离!!!// 执行Service函数TcpServer::ThreadData *td = static_cast<TcpServer::ThreadData *>(args);td->_this->_service(td->_sockfd, td->_addr);td->_sockfd->Close();delete td;return nullptr;}

就可以解析出来套接字文件描述符和客户端信息了!解析出信息之后就去执行会话层的回调函数进行IO操作:

  1. Service内部只有一个成员变量,就是应用层的回调函数,Service解析出来数据之后就可以传入到应用层中进行使用
  2. IO中主要需要进行从sockfd文件中获取数据,然后通过协议进行解析,获取到真正的数据。再调用回调函数对数据进行操作!得到结果之后就可以进行序列化,加入报头,再发送给客户端!
  3. 应用层的操作逻辑,Service并不关心,只要回调函数可以传回需要的结构体就可以!
class Service
{
public:Service(process_t process) : _process(process){}void IOExecute(SockSPtr sock, InetAddr &addr){LOG(INFO, "service start!!!\n");std::string message;while (true){// 1. 进行读取ssize_t n = sock->Recv(&message);if (n < 0){LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}// 此时获取到客户端发送的数据// 但是不能保证是否是完整的报文// 2.报文解析std::string str = Decode(message); // 通过去报头获取报文if (str.empty())continue; // 说明没有完整的报文!// 到这里说明有完整的报文!!!auto req = Factory::BuildRequestDefault();// 3.反序列化初始化Requestreq->Deserialize(str);auto res = Factory::BuildResponseDefault();// 4.业务处理res = _process(req);// 5.进行序列化处理std::string ret;res->Serialize(&ret);// 6.加入报头Encode(ret);// 7.将获取的数据发送回去sock->Send(ret);}}~Service(){}private:process_t _process;
};

4 应用层 — 网络计算器

应用层根据具体需要可以随时改变,我这里以网络计算器为例子进行书写:

#include "Protocol.hpp"
class NetCal
{
public:NetCal() {}std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req){std::shared_ptr<Response> res = Factory::BuildResponseDefault();switch (req->Oper()){case '+':res->_res = req->X() + req->Y();res->_code = 0;res->_desc = "success";break;case '-':res->_res = req->X() - req->Y();res->_code = 0;res->_desc = "success";break;case '*':res->_res = req->X() * req->Y();res->_code = 0;res->_desc = "success";break;case '/':{if (req->Y() == 0){res->_code = 1;res->_desc = "div zero";}res->_res = req->X() / req->Y();res->_code = 0;res->_desc = "success";}break;case '%':{if (req->Y() == 0){res->_code = 1;res->_desc = "mod zero";}res->_res = req->X() % req->Y();res->_code = 0;res->_desc = "success";}break;default:res->_code = 2;res->_desc = "illegal operations";break;}return res;}~NetCal() {}
};

逻辑很简单不在多加赘述!

5 总结

现在我们的程序分为了三层结构:
在这里插入图片描述
我们做到了最大程度的解耦!

  • 传输层只负责获取链接,我们应用层要进行什么工作,只要是进行网络通信传输层的工作就是唯一的!
  • 会话层进行IO操作!只要传输层提供了链接,会话层就可以获取数据,然后根据具体的协议进行数据的解析工作。协议根据实际情况改变,但是会话层的工作逻辑是不变的!
  • 应用层只管进行数据处理即可,什么但不不需要考虑!完成工作后返回给会话层数据即可!

这样的结构逻辑十分清晰,并且解耦的非常优雅,值得反复品味!!!

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

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

相关文章

WPF DataGrid 单元格居中,头部居中,点击行改变背景色。

我得全局样式都写在了App.XAML文件下的ResourceDictionary里&#xff0c;方便全局引用 DataGrid样式和点击改变行背景色的触发器(BasedOn继承的是UI框架的样式&#xff0c;若无则删除&#xff0c;触发器还有鼠标移动事件等&#xff0c;按需自行修改添加) <Style x:Key&quo…

安卓13长按电源按键直接关机 andriod13不显示关机对话框直接关机

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 有些设备需要在长按电源键的时候,直接关机。不需要弹出对话框进行询问。 2.问题分析 过滤电源按键,需要在系统里面处理的话,那么我们需要熟悉android的事件分发,然后再…

Golang | Leetcode Golang题解之第420题强密码检验器

题目&#xff1a; 题解&#xff1a; func strongPasswordChecker(password string) int {hasLower, hasUpper, hasDigit : 0, 0, 0for _, ch : range password {if unicode.IsLower(ch) {hasLower 1} else if unicode.IsUpper(ch) {hasUpper 1} else if unicode.IsDigit(ch)…

【2025】儿童疫苗接种预约小程序(源码+文档+解答)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

hackmyvm靶场--zon

环境 攻击机kali 靶机 未知 主机探测 因为在同一个局域网内使用ARP协议探测存活主机 靶机为192.168.56.128 端口探测 常见的80和22端口 那么一定是寻找web漏洞拿shell了 后台扫描 后台扫描常用dirsearch和gobuster,有时候小字典可能不太行&#xff0c;可以尝试换个大点…

使用AVL树实现Map

一、数组在裂变扩容时可能会出现环、在数组元素转为链表之后选择尾插法插入节点、数组到链表到AVL到RBT的转换 1、数组在裂变扩容时链表中的节点计算出来的位置可能也会发生变化&#xff0c;在多线程情况下调整节点位置可能会出现环。 2、数组中的数组元素转为链表后插入新节点…

设计模式 享元模式(Flyweight Pattern)

享元模式 简绍 享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;它的目的是通过共享技术来有效地支持大量细粒度的对象。享元模式可以极大地减少内存的使用&#xff0c;从而提高程序的性能。它特别适用于需要创建大量相似对象的场景&#…

Cypress安装与启动(开始学习记录)

一 Cypress安装 使用npm安装 1.查看node.js npm的版本&#xff0c;输入 npm --version 和 node --version&#xff0c;node.js没安装的可以去中文网下载最新稳定版安装&#xff0c;npm不建议升级到最新版本&#xff0c;会导致安装Cypress时Error: Cannot find module ansi-st…

在已安装的openresty上添加安装upstream模块报错的解决以及使用Consul服务发现时定时变更nginx的upstream的shell脚本

一、在已经安装好的openresty环境上添加安装upstream模块报错&#xff1a; 在已经安装好的openresty环境上添加安装upstream模块报错&#xff1a;http upstream check module can not find any check server, make sure you ve added the check 的问题解决。 服务器上已经安装好…

idea中java及java web项目的常见问题

1、乱码问题&#xff0c;主要有几处地方&#xff0c;需要检查。 ①确保文件编码&#xff0c;其实主要就是在idea启动文件中&#xff0c;增加了 -Dfile.encodingUTF-8的设置 ②编辑器默认编码&#xff0c;都改为UTF-8 ③Tomcat的运行配置&#xff0c;编码也改为UTF-8,同样使用…

SpringBoot3核心特性-核心原理

目录 传送门前言一、事件和监听器1、生命周期监听2、事件触发时机 二、自动配置原理1、入门理解1.1、自动配置流程1.2、SPI机制1.3、功能开关 2、进阶理解2.1、 SpringBootApplication2.2、 完整启动加载流程 三、自定义starter1、业务代码2、基本抽取3、使用EnableXxx机制4、完…

zynq的PS端mac与RTL8211F的连接要点

目录 1 VCCO_MIO12 PS_MIO_VREF3 PS的引脚4 RXDLY TXDLY5 ZYNQ的MAC可以调整延时吗 1 VCCO_MIO1 接1.8V 2 PS_MIO_VREF 接0.9V&#xff0c;可通过电阻分压 可通过电阻分压 3 PS的引脚 4 RXDLY TXDLY RXDLY RXD[0] TXDLY RXD[1] 与XC7Z020的PS端MAC连接&#xff0c;必须…

CVE-2024-2389 未经身份验证的命令注入

什么是 Progress Flowmon? Progress Flowmon 是一种网络监控和分析工具,可提供对网络流量、性能和安全性的全面洞察。Flowmon 将 Nette PHP 框架用于其 Web 应用程序。 未经身份验证的路由 我们开始在“AllowedModulesDecider.php”文件中枚举未经身份验证的端点,这是一个描…

1.pytest基础知识(默认的测试用例的规则以及基础应用)

一、pytest单元测试框架 1&#xff09;什么是单元测试框架 单元测试是指再软件开发当中&#xff0c;针对软件的最小单位&#xff08;函数&#xff0c;方法&#xff09;进行正确性的检查测试。 2&#xff09;单元测试框架 java&#xff1a;junit和testing python&#xff1a;un…

arcgisPro地理配准

1、添加图像 2、在【影像】选项卡中&#xff0c;点击【地理配准】 3、 点击添加控制点 4、选择影像左上角格点&#xff0c;然后右击填入目标点的投影坐标 5、依次输入四个格角点的坐标 6、点击【变换】按钮&#xff0c;选择【一阶多项式&#xff08;仿射&#xff09;】变换 7…

基于SpringBoot+定时任务实现地图上绘制车辆实时运动轨迹图

目录 1. 项目结构 2. Maven依赖配置 (pom.xml) 3. 实现后端服务 4. 配置文件 (application.properties) 5. 启动项目 6. 访问页面 实现基于北斗卫星的车辆定位和轨迹图的Maven工程&#xff08;使用模拟数据&#xff09;&#xff0c;我们将使用以下技术&#xff1a; Spri…

使用c#制作一个小型桌面程序

封装dll 首先使用visual stdio 创建Dll新项目,然后属性管理器导入自己的工程属性表&#xff08;如果没有可以参考visual stdio 如何配置opencv等其他环境&#xff09; 创建完成后 系统会自动生成一些文件&#xff0c;其中 pch.cpp 先不要修改&#xff0c;pch.h中先导入自己需…

蓝牙模块—BLE-CC41-A

1. 蓝牙的特点 蓝牙模块采用的 TI 公司设计的 CC2541芯片&#xff0c;主要面向低功耗蓝牙通信方案&#xff0c;该模块的工作频段为 2.4Ghz&#xff0c;这个频段属于国际通用频段 注意&#xff1a;蓝牙集成了一个状态指示灯&#xff0c;LED灯如果均匀慢速闪烁&#xff0c;就表示…

【渗透测试】-vulnhub源码框架漏洞-Os-hackNos-1

vulnhub源码框架漏洞中的CVE-2018-7600-Drupal 7.57 文章目录  前言 1.靶场搭建&#xff1a; 2.信息搜集&#xff1a; 主机探测&#xff1a; 端口扫描&#xff1a; 目录扫描&#xff1a; 3.分析&#xff1a; 4.步骤&#xff1a; 1.下载CVE-2018-7600的exp 2.执行exp: 3.写入木…

Docker安装rabbitmq并配置延迟队列

下载rabbitmq镜像 docker pull rabbitmq:management 运行rabbitmq镜像 docker run -id --namerabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 -e RABBITMQ_DEFAULT_USERtom -e RABBITMQ_DEFAULT_PASStom rabbitmq:management …