【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构
从零开始掌握序列化
- 1 知识回顾
- 2 序列化与编写协议
- 2.1 使用Json进行序列化
- 2.2 编写协议
- 3 封装IOService
- 4 应用层 --- 网络计算器
- 5 总结
1 知识回顾
上一篇文章我们讲解了协议的本质是双方能够看到的结构化数据。并通过传输层的底层理解了为什么read系列函数时全双工支持同时读写的:TCP传输层有两个缓冲区,分别接收和发送。最重要的是我们将TCP通信的代码进行的重构:
- 我们将Socket通信单独封装为一个类,负责Socket套接字的创建,bind绑定服务器端口号,进入监听模式…工作,基类Socket并不进行定义,只进行声明!具体实现由派生类TcpServer和UdpServer来进行
- TcpServer继承Socket类的所有方法,然后进行具体的函数定义!
- 上层的TcpServer直接底层使用TcpSocket对象就可以完成Socket系列操作,十分方便!
接下来我们要实现是这样的一个结构:
通信过程整体分为三层
- 传输层TcpServer:负责从Socket文件中获取链接,传输层不需要进行IO,获取到连接就让会话层通过连接获取数据!
- 会话层Service:根据传输层给的连接,从Sockfd文件中读取数据,解析出报文结构中的数据字符串,然后通过协议分离出结构化数据。该层只负责数据的解析,数据的处理交给应用层进行!
- 应用层Process:应用层是具有的业务逻辑,根据会话层解析出的数据,进行数据处理!
这样是一个非常非常优雅的封装操作!!!
2 序列化与编写协议
2.1 使用Json进行序列化
协议是IO的基础,只有协议确定下来,才可以进行通信。
我们这里想要实现一个网络计算器的应用,所以协议分为了两个类:Request和Response。分别作为传入的数据和传出的数据:
- Request:两个数字和一个运算符
- Response:结果数字 , 错误码 ,退出信息
他们是作为结构化的数据进行传输,那么想要进行传输就来到了最重要的部分序列化与反序列化!序列化与反序列化可以使用第三方库也可以自己进行编写。这里我们先使用第三方的Json库进行实现:
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp 是开源的, 广泛用于各种需要处理 JSON 数据的 C++ 项目中:
- 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。
- 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。
- 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数字、 布尔值和 null。
- 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便开发者调试
在Linux中使用需要进行安装对应的JSON库:
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
安装之后就可以进行使用了:
使用起来是十分方便的:
- Json::Value是最重要的类,这是对Json数据结构进程操作和表示的关键类
- 建立好类Json::Value之后就可以通过
[ ]
操作root["x"] = _x;
,像这样就可以进行赋值- 将Json数据结构转换为字符串依靠 Json::FastWriter 或 Json::StreamWriter都可以转换成字符串
Json::StyledWriter writer; std::string s = writer.write(root)
- 通过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操作我们就可以把协议写出来,代码虽然很长但是很好理解:
- Request类中需要根据
int x , int y , char oper
进行序列化生成字符串,也要能够通过字符串反序列化得到三个变量 - 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操作:
- Service内部只有一个成员变量,就是应用层的回调函数,Service解析出来数据之后就可以传入到应用层中进行使用
- IO中主要需要进行从sockfd文件中获取数据,然后通过协议进行解析,获取到真正的数据。再调用回调函数对数据进行操作!得到结果之后就可以进行序列化,加入报头,再发送给客户端!
- 应用层的操作逻辑,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实现协议 + 设计 传输\会话\应用 三层结构
唯有梦想才配让你不安, 唯有行动才能解除你的不安。 --- 卢思浩 --- 从零开始掌握序列化 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,和flask一样 2. 关于如何利用vue创建web,请查看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.需求/要求: 通过一台PC,一台交换机,两台路由器来成功实现内网访问外网。理解NAT的转换机制。 二、实验过程 1.拓扑图: 2.步骤: 1.PC1配置ip地址及网关: 2.AR1接口配置ip地址࿱…...

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

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; // 定义圈数,最少1圈int count 1; // 用来给矩阵中…...

Matlab Simulink 主时间步(major time step)、子时间步(minor time step)
高亮颜色说明:突出重点 个人觉得,:待核准个人观点是否有误 高亮颜色超链接 文章目录 对Simulink 时间步的理解Simulink 采样时间的类型Discrete Sample Times(离散采样时间)Controllable Sample Time(可控采样时间) Continuous Sample Times(…...

docker 升级步骤
Docker 升级的步骤通常取决于你所使用的操作系统。以下是针对常见操作系统(如 Ubuntu 和 CentOS)的 Docker 升级步骤: Ubuntu 更新现有的包索引: sudo apt-get update 升级 Docker: 您可以运行以下命令来升级 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 协议又叫爬虫协议,访问 robots.txt 继续访问 /JAY.php 拿到 flag:flag{hello_Do_YOU_KONw_JAY!} 2、NOF12 F12 和右键都被禁用 方法&#…...

亲测全网10大“免费”论文降重神器!论文写作必备!
在当今学术研究和论文写作中,AI技术的应用已经变得越来越普遍。为了帮助学者们更高效地完成论文撰写任务,以下将详细介绍十款必备的论文写作工具,其中特别推荐千笔-AIPassPaper。 1. 千笔-AIPassPaper 千笔-AIPassPaper是一款基于深度学习和…...

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

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

《粮油与饲料科技》是什么级别的期刊?是正规期刊吗?能评职称吗?
问题解答 问:《粮油与饲料科技》是不是核心期刊? 答:不是,是知网收录的第一批认定 学术期刊。 问:《粮油与饲料科技》级别? 答:省级。主管单位:中文天地出版传媒集团股份有限公司…...

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

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

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

尚品汇-秒杀商品定时任务存入缓存、Redis发布订阅实现状态位(五十一)
目录: (1)秒杀业务分析 (2)搭建秒杀模块 (3)秒杀商品导入缓存 (4)redis发布与订阅实现 (1)秒杀业务分析 需求分析 所谓“秒杀”࿰…...

第十一章 【后端】商品分类管理微服务(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. 二叉树(BinaryTree) 3.2. 红黑树(RB&a…...

通用接口开放平台设计与实现——(31)API服务线程安全问题确认与修复
背景 在本系列的前面一篇博客评论中,有小伙伴指出,API服务存在线程安全问题: https://blog.csdn.net/seawaving/article/details/122905199#comments_34477405 今天来确认下,线程是否安全?如不安全,如何…...

2011-2022年数字金融与企业ESG表现:效应、机制与“漂绿”检验(内含原始数据+处理代码)
2011-2022年数字金融与企业ESG表现:效应、机制与“漂绿”检验(内含原始数据处理代码) 1、时间:2011-2022年 2、来源:上市公司年报、华证ESG、北大数字普惠金融 3、指标:年份、股票代码、股票简称、行业名…...

mysql配置相关命令
一、允许所有人访问: -- 1.切换至mysql库 use mysql;-- 2.查看用户表 SELECT Host,User FROM user;-- 3.修改字段 UPDATE user SET Host % WHERE User root;-- 4.刷新权限 flush privileges;二、修改加密方式 -- 1.切换至mysql库 use mysql;-- 2.查看用户表 SELEC…...

【自用软件】IDM下载器 Internet Download Manager v6.42 Build 10
下载IDM&pj安装教程 Internet Download Manager,简称 IDM,是国外的一款优秀下载工具。目前凭借着下载计算的速度优势在外媒网站中均受好评,现在已被多数国人熟知。Internet Download Manager 提升你的下载速度最多达5倍,安排下…...

Kafka集群扩容(新增一台kafka节点)
kafka集群扩容、kafka topic迁移 现有环境 IP组件角色192.168.17.51kafka01broker1192.168.17.52kafka02broker2192.168.17.53kafka03broker3 扩容之后环境 IP组件角色192.168.17.51kafka01broker1192.168.17.52kafka02broker2192.168.17.53kafka03broker3192.168.17.54ka…...

作文笔记15 点面结合
事件中场面写作方法:点面结合(对毛主席的描写和三十万群众的描写间插进行)。好处是强化描写的层次感,既有整体形象描写,又凸显人物个性特点。 景色描写方法:动态描写,静态描写,动静…...

Spring Boot-国际化(I18N)问题
Spring Boot 国际化(I18N)问题及其解决方案 1. 引言 随着全球化的推进,软件开发中的国际化(I18N)需求日益增长。国际化是指通过设计应用程序,使其能够轻松适应不同语言和地区的需求,而无需修改…...

8. 防火墙
8. 防火墙 (1) 防火墙的类型和结构 防火墙的类型和结构可以根据其在网络协议栈中的过滤层次和实现方式进行分类。常见的防火墙类型包括: 包过滤防火墙:工作在网络层(OSI模型的第3层),主要检查IP包头的信息,如源地址、目的地址、端口号等。电路级网关防火墙:工作在会话层…...

C语言循环学习
作为初学者,学习C语言中的循环结构是非常重要的,它们能让你轻松地重复执行代码。在C语言中,常用的循环结构主要有for循环和while循环。我们将从基本概念开始,逐步讲解如何使用这两种循环,并通过示例帮助你理解和练习。…...

职业技能大赛-自动化测试笔记(Unitest)分享-3
前言 UnitTest是Python标准库中的一个模块,用于编写和执行单元测试。它提供了一组断言方法,用于验证代码的输出和状态是否符合预期。通过UnitTest框架,我们可以编写可重复执行的测试用例,并使用命令行工具或IDE轻松运行这些测试。在大多数情况下,UnitTest框架已经包含在Py…...