【计网】从零开始掌握序列化 --- 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…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...

DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...
字符串哈希+KMP
P10468 兔子与兔子 #include<bits/stdc.h> using namespace std; typedef unsigned long long ull; const int N 1000010; ull a[N], pw[N]; int n; ull gethash(int l, int r){return a[r] - a[l - 1] * pw[r - l 1]; } signed main(){ios::sync_with_stdio(false), …...
背包问题双雄:01 背包与完全背包详解(Java 实现)
一、背包问题概述 背包问题是动态规划领域的经典问题,其核心在于如何在有限容量的背包中选择物品,使得总价值最大化。根据物品选择规则的不同,主要分为两类: 01 背包:每件物品最多选 1 次(选或不选&#…...

基于django+vue的健身房管理系统-vue
开发语言:Python框架:djangoPython版本:python3.8数据库:mysql 5.7数据库工具:Navicat12开发软件:PyCharm 系统展示 会员信息管理 员工信息管理 会员卡类型管理 健身项目管理 会员卡管理 摘要 健身房管理…...