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

【计网】从零开始掌握序列化 --- 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操作!只要传输层提供了链接,会话层就可以获取数据,然后根据具体的协议进行数据的解析工作。协议根据实际情况改变,但是会话层的工作逻辑是不变的!
  • 应用层只管进行数据处理即可,什么但不不需要考虑!完成工作后返回给会话层数据即可!

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

相关文章:

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

唯有梦想才配让你不安&#xff0c; 唯有行动才能解除你的不安。 --- 卢思浩 --- 从零开始掌握序列化 1 知识回顾2 序列化与编写协议2.1 使用Json进行序列化2.2 编写协议 3 封装IOService4 应用层 --- 网络计算器5 总结 1 知识回顾 上一篇文章我们讲解了协议的本质是双方能够…...

Qt 模型视图(四):代理类QAbstractItemDelegate

文章目录 Qt 模型视图(四):代理类QAbstractItemDelegate1.基本概念1.1.使用现有代理1.2.一个简单的代理 2.提供编辑器3.向模型提交数据4.更新编辑器的几何图形5.编辑提示 Qt 模型视图(四):代理类QAbstractItemDelegate ​ 模型/视图结构是一种将数据存储和界面展示分离的编程方…...

django+vue

1. diango 只能加载静态js&#xff0c;和flask一样 2. 关于如何利用vue创建web&#xff0c;请查看flask vue-CSDN博客 3. 安装django pip install django 4. 创建新项目 django-admin startproject myproject 5.django 中可以包含多个app 5.1 创建一个app cd myprojec…...

HCIA--实验十七:EASY IP的NAT实现

一、实验内容 1.需求/要求&#xff1a; 通过一台PC&#xff0c;一台交换机&#xff0c;两台路由器来成功实现内网访问外网。理解NAT的转换机制。 二、实验过程 1.拓扑图&#xff1a; 2.步骤&#xff1a; 1.PC1配置ip地址及网关&#xff1a; 2.AR1接口配置ip地址&#xff1…...

彻底解决:QSqlDatabase: QMYSQL driver not loaded

具体错误 QSqlDatabase: QMYSQL driver not loaded QSqlDatabase: available drivers: QSQLITE QMIMER QMARIADB QMYSQL QODBC QPSQL 检查驱动 根据不同安装目录而不同&#xff1a; D:\Qt\6.7.2\mingw_64\plugins\sqldrivers 编译驱动 如果没有&#xff0c;需要自行编译&…...

leetcode02——59. 螺旋矩阵 II、203. 移除链表元素

59. 螺旋矩阵 II class Solution {public int[][] generateMatrix(int n) {int[][] nums new int[n][n]; // 定义二维数组用于存储数据int startX 0; // 定义每循环一个圈的起始位置int startY 0;int loop 1; // 定义圈数&#xff0c;最少1圈int count 1; // 用来给矩阵中…...

Matlab Simulink 主时间步(major time step)、子时间步(minor time step)

高亮颜色说明&#xff1a;突出重点 个人觉得&#xff0c;&#xff1a;待核准个人观点是否有误 高亮颜色超链接 文章目录 对Simulink 时间步的理解Simulink 采样时间的类型Discrete Sample Times(离散采样时间)Controllable Sample Time(可控采样时间) Continuous Sample Times(…...

docker 升级步骤

Docker 升级的步骤通常取决于你所使用的操作系统。以下是针对常见操作系统&#xff08;如 Ubuntu 和 CentOS&#xff09;的 Docker 升级步骤&#xff1a; Ubuntu 更新现有的包索引&#xff1a; sudo apt-get update 升级 Docker&#xff1a; 您可以运行以下命令来升级 Docker…...

828华为云征文 | 云服务器Flexus X实例:one-api 部署,支持众多大模型

目录 一、one-api 介绍 二、部署 one-api 2.1 拉取镜像 2.2 部署 one-api 三、运行 one-api 3.1 添加规则 3.2 运行 one-api 四、添加大模型 API 4.1 添加大模型 API 五、总结 本文通过 Flexus云服务器X实例 部署 one-api。Flexus云服务器X实例是新一代面向中小企业…...

2024 SNERT 预备队招新 CTF 体验赛-Web

目录 1、robots 2、NOF12 3、get_post 4、好事慢磨 5、uploads 6、rce 7、ezsql 8、RCE 1、robots robots 协议又叫爬虫协议&#xff0c;访问 robots.txt 继续访问 /JAY.php 拿到 flag&#xff1a;flag{hello_Do_YOU_KONw_JAY!} 2、NOF12 F12 和右键都被禁用 方法&#…...

亲测全网10大“免费”论文降重神器!论文写作必备!

在当今学术研究和论文写作中&#xff0c;AI技术的应用已经变得越来越普遍。为了帮助学者们更高效地完成论文撰写任务&#xff0c;以下将详细介绍十款必备的论文写作工具&#xff0c;其中特别推荐千笔-AIPassPaper。 1. 千笔-AIPassPaper 千笔-AIPassPaper是一款基于深度学习和…...

二分算法——优选算法

个人主页&#xff1a;敲上瘾-CSDN博客 个人专栏&#xff1a;游戏、数据结构、c语言基础、c学习、算法 本章我们来学习的是二分查找算法&#xff0c;二分算法的应用非常广泛&#xff0c;不仅限于数组查找&#xff0c;还可以用于解决各种搜索问题、查找极值问题等。在数据结构和算…...

Kafka 的基本概念

一、Kafka 主要用来做什么 作为消息系统&#xff1a;Kafka 具备系统解藕&#xff0c;流量削峰&#xff0c;缓冲&#xff0c;异步通信&#xff0c;扩展性&#xff0c;可恢复性等功能&#xff0c;以及消息顺序性保障和回溯消费 作为存储系统&#xff1a;Kafka 把消息持久化到磁…...

《粮油与饲料科技》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问&#xff1a;《粮油与饲料科技》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第一批认定 学术期刊。 问&#xff1a;《粮油与饲料科技》级别&#xff1f; 答&#xff1a;省级。主管单位&#xff1a;中文天地出版传媒集团股份有限公司…...

Python之一些列表的练习题

1.比较和对比字符串、列表和元组。例如&#xff0c;它们可以容纳哪类内容以及在数据结构上可以做哪些操作。 1. 内容类型:- 字符串: 只能包含字符(文本)。- 列表: 可以包含任意类型的数据,如数字、字符串、其他列表等。- 元组: 可以包含任意类型的数据,与列表类似。3. 操作:(1…...

MoFA: 迈向AIOS

再一次向朋友们致以中秋的祝福&#xff01; MoFA (Modular Framework for Agents)是一个独特的模块化AI智能体框架。MoFA以组合&#xff08;Composition)的逻辑和编程&#xff08;Programmable&#xff09;的方法构建AI智能体。开发者通过模版的继承、编程、定制智能体&#xf…...

c语言中define使用方法

在C语言中&#xff0c;#define指令是预处理指令&#xff0c;用于定义宏。其常用格式是&#xff1a; 定义常量&#xff1a; #define 常量名 常量值 例子&#xff1a; #define PI 3.14159 #define MAX_SIZE 100 这里&#xff0c;PI和MAX_SIZE在代码中会被替换为其对应的值。没有…...

尚品汇-秒杀商品定时任务存入缓存、Redis发布订阅实现状态位(五十一)

目录&#xff1a; &#xff08;1&#xff09;秒杀业务分析 &#xff08;2&#xff09;搭建秒杀模块 &#xff08;3&#xff09;秒杀商品导入缓存 &#xff08;4&#xff09;redis发布与订阅实现 &#xff08;1&#xff09;秒杀业务分析 需求分析 所谓“秒杀”&#xff0…...

第十一章 【后端】商品分类管理微服务(11.4)——spring-boot-devtools

11.4 spring-boot-devtools 官网:https://docs.spring.io/spring-boot/reference/using/devtools.html Spring Boot DevTools 是 Spring Boot 提供的一组易于使用的工具,旨在加速开发和测试过程。它通过提供一系列实用的功能,如自动重启、实时属性更新、依赖项的热替换等,…...

MySQL篇(索引)(持续更新迭代)

目录 一、简介 二、有无索引情况 1. 无索引情况 2. 有索引情况 3. 优劣势 三、索引结构 1. 简介 2. 存储引擎对于索引结构的支持情况 3. 为什么InnoDB默认的索引结构是Btree而不是其它树 3.1. 二叉树&#xff08;BinaryTree&#xff09; 3.2. 红黑树&#xff08;RB&a…...

vscode里如何用git

打开vs终端执行如下&#xff1a; 1 初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器

——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的​​一体化测试平台​​&#xff0c;覆盖应用全生命周期测试需求&#xff0c;主要提供五大核心能力&#xff1a; ​​测试类型​​​​检测目标​​​​关键指标​​功能体验基…...

在rocky linux 9.5上在线安装 docker

前面是指南&#xff0c;后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 原创笔记&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 上一篇&#xff1a;《数据结构第4章 数组和广义表》…...

Appium下载安装配置保姆教程(图文详解)

目录 一、Appium软件介绍 1.特点 2.工作原理 3.应用场景 二、环境准备 安装 Node.js 安装 Appium 安装 JDK 安装 Android SDK 安装Python及依赖包 三、安装教程 1.Node.js安装 1.1.下载Node 1.2.安装程序 1.3.配置npm仓储和缓存 1.4. 配置环境 1.5.测试Node.j…...

数据挖掘是什么?数据挖掘技术有哪些?

目录 一、数据挖掘是什么 二、常见的数据挖掘技术 1. 关联规则挖掘 2. 分类算法 3. 聚类分析 4. 回归分析 三、数据挖掘的应用领域 1. 商业领域 2. 医疗领域 3. 金融领域 4. 其他领域 四、数据挖掘面临的挑战和未来趋势 1. 面临的挑战 2. 未来趋势 五、总结 数据…...

Springboot 高校报修与互助平台小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;高校报修与互助平台小程序被用户普遍使用&#xff0c;为…...

Vue 实例的数据对象详解

Vue 实例的数据对象详解 在 Vue 中,数据对象是响应式系统的核心,也是组件状态的载体。理解数据对象的原理和使用方式是成为 Vue 专家的关键一步。我将从多个维度深入剖析 Vue 实例的数据对象。 一、数据对象的定义方式 1. Options API 中的定义 在 Options API 中,使用 …...