【计网】自定义序列化反序列化(三) —— 实现网络版计算器【下】
🌎实现网络版计算器【下】
本次序列化与反序列化所用到的代码,Tcp服务自定义序列化反序列化实现网络版计算器。
文章目录:
实实现网络版计算器【下】
客户端实现
基于守护进程的改写
🚀客户端实现
在这之前,我们已经将服务器端的代码部分做好了准备,现在万事俱备只欠客户端发起连接,而客户端在这里不准备那么多的封装了,与之前写的客户端相同,我们想要客户端以 ./cal_client ip port
的形式来创建客户端:
#include <iostream>
#include <string>
#include <memory>
#include <ctime>#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}using namespace socket_ns;
using namespace protocol_ns;// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];// 服务器端ipuint16_t serverport = std::stoi(argv[2]);// 服务器端portreturn 0;
}
而Udp客户端构建对象就不需要这么麻烦了,因为我们早在最开始已经将Socket类进行了封装,这样我们就不需要在调用原生接口在客户端裸露式调用:
InetAddr serveraddr(serverip, serverport);// 通过ip和port构建InetAddr对象
std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();// 子类对象构造父类指针方便多态式调用
bool res = cli->BuildClientSocket(serveraddr);// 建立客户端Socket 链接
此时,客户端的Socket服务就已经构建完毕,网络通信已经做好准备,只要服务器通,客户端随时都可以发起连接。接着就是处理客户端的业务逻辑。
我们要知道客户端是要给服务器端发送请求并且获取相应的一个过程,获取成功之后将响应进行反序列化拿到最终的结果。为了方便测试,我们这里让客户端采用固定的提问方式不断对客户端发送请求获取响应并且解析,我们将构建请求以及接收响应封装为一个 Factory类。
其中,客户端请求让 x 为 1-10的随机数,y为 0-4的随机数,让他们进行模运算,并将计算构造为Request类,返回值为Request的指针:
class Factory
{
public:Factory(){srand(time(nullptr) ^ getpid());opers = "+/*/%^&|";}std::shared_ptr<Request> BuildRequest(){int x = rand() % 10 + 1;usleep(x * 10);int y = rand() % 5; // [0,1,2,3,4]usleep(y * x * 5);char oper = opers[rand() % opers.size()];std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);return req;}std::shared_ptr<Response> BuildResponse(){return std::make_shared<Response>();}~Factory(){}private:std::string opers;// 操作数: +/-/*/\/% ...
};
为了方便测试,我们这里只启动一个客户端,这个客户端不停的给服务器发送数据,所以我们需要将待发送请求以及返回的响应放在while循环内不断发送获取解析。
我们想要积压一批数据,然后在一次性发送,这样就能测试服务器的功能是否有问题,是否能处理多批数据,所以在这里我们一次性构建五个请求让后在发送给服务器端,同样构建请求时需要对数据进行序列化和添加长度报头:
Factory factory;
std::string inbuffer;while (res)
{sleep(1);// 构建请求std::string str;for (int i = 0; i < 5; ++i)// 一次性构建5个请求{auto req = factory.BuildRequest();// 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "Serialize: \n"<< send_str << std::endl;// 添加长度报头send_str = Encode(send_str);std::cout << "Encode: \n"<< send_str << std::endl;str += send_str;}// "len"\r\n"{}"\r\ncli->Send(str);// 发送请求
}
我们将求情发送之后,客户端就静静等待服务器端返回的响应,当然,与服务器端接收消息相同,客户端接收的每条应答一定就是完整的应答吗?不一定,所以我们将读取到的数据进行Decode(),这样我们对所有的应答进行解析,如果是一条完整的应答Decode接口就会返回一个response对象,对象里就是解析过后一条完整的应答内容:
while (res)
{sleep(1);// 构建请求std::string str;for (int i = 0; i < 5; ++i){auto req = factory.BuildRequest();// 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "Serialize: \n"<< send_str << std::endl;// 添加长度报头send_str = Encode(send_str);std::cout << "Encode: \n"<< send_str << std::endl;str += send_str;}// "len"\r\n"{}"\r\ncli->Send(str);// 读取应答int n = cli->Recv(&inbuffer);if (n <= 0)break;std::string package = Decode(inbuffer);if (package.empty())continue;
}
那么此后,我们获取的应答就一定是一条完整的应答,但是这个应答此时还是序列化状态,我们需要将其进行反序列化处理,最后输出响应结果即可:
while (res)
{sleep(1);// 构建请求std::string str;for (int i = 0; i < 5; ++i){auto req = factory.BuildRequest();// 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "Serialize: \n"<< send_str << std::endl;// 添加长度报头send_str = Encode(send_str);std::cout << "Encode: \n"<< send_str << std::endl;str += send_str;}// "len"\r\n"{}"\r\ncli->Send(str);// 读取应答int n = cli->Recv(&inbuffer);if (n <= 0)break;std::string package = Decode(inbuffer);if (package.empty())continue;// 读到的package一定是一个完整的应答auto resp = factory.BuildResponse();// 反序列化resp->Deserialize(package);// 拿到了结构化的应答std::cout << resp->_result << "[" << resp->_code << "]" << std::endl;
}
为了更好地体现服务器端对报文的处理是否正确,我们在TcpServerMain内的Service服务进行细微调整,前面,我们让客户端不断地对服务器端发出请求,那么服务器端的Service也要不断地去处理请求,并发送到客户端:
void ServiceHelper(socket_sptr sockptr, InetAddr client)
{int sockfd = sockptr->SockFd();LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";std::string inbuffer;while (true){sleep(5);Request req;// 1. 读取数据int n = sockptr->Recv(&inbuffer);if (n < 0){LOG(DEBUG, "client %s quit", clientaddr.c_str());break;}// 2. 分析数据std::string package;while (true){sleep(1);std::cout << "inbuffer" << inbuffer << std::endl;package = Decode(inbuffer);if (package.empty())break;std::cout << "-----------------------begin----------------------" << std::endl;std::cout << "resq string:\n"<< package << std::endl;// 3.反序列化req.Deserialize(package);// 4. 业务处理Response resp = _cb(req);// 5. 对应答进行序列化std::string send_str;resp.Serialize(&send_str);std::cout << send_str << std::endl;// 6. 添加长度报头send_str = Encode(send_str);// 7. 发送到对端sockptr->Send(send_str);}}
}
那么我们所有准备都已经做好了,接下来就是通信时刻:
这样网路版本计算器我们就实现完成了。在这个项目当中,我们发现,我们把从Tcp内读取的报文,可能读到半个,可能读到一个半,或者其他特殊不完整报文情况,这种情况我们称为 Tcp粘包问题。而我们使用Encode() 和 Decode() 接口就是为了解决tcp粘包问题的。
🚀基于守护进程的改写
我们知道,我们在连接远程服务器的时候,实际上就是打开一个终端文件,如果有多个连接就会打开多个终端文件,我们从一台设备向另一台设备进行重定向的时候就是如此:
并且我们可以将消息发送到另外一个终端文件当中,使用如下命令进行重定向:
echo "message" >> /dev/pts/n #这里n指的是任何一个存在的终端文件
这里如果你是使用XShell来测试上面的命令,你很可能不会成功,因为版本升级的原因,但是我们能知道这个现象就行。总而言之,当我们连接Linux服务器的时候,会给我们打开一个终端文件,再启动bash命令行解释器。
首先是创建终端文件,其次bash被启动,而bash又作为所有进程的父进程,bash则会打开创建的终端文件。而一般终端文件与启动的bash会被打包称为一个 会话(具体在以后守护进程章节中看到),而每个会话都会有自己的 会话id(sid),而一般 会话的id是终端中的第一个进程的pid也就是bash:
但是,只要在当前终端下启动的任何服务(进程),都属于当前的会话!比如:
所以会话就像是bash进程中的管理容器,如果一个会话被销毁了,那么会话里的所有进程也都会终止,
但是今天,我想要一种不受会话影响的进程,也就是不受用户登录退出的影响,独立于会话之外的进程,比如我们的网络版计算器服务器端,我们不想让其受用户注册销毁的影响,所以我们可以编写代码将其变为 守护进程。
实际上这么做的意义就是创建一个新的会话,在Linux中给我们提供了 setsid()
接口:
setsid()会创建出一个新的会话,不过有一个要求:调用进程不能是进程组的组长。那么什么是进程组呢?很简单,每个进程组都有一个唯一的标识符,通常是进程组的组长(Leader)的进程ID,而组长就是他们之中第一个启动的进程:
所以,我们在程序中直接创建子进程,并且退出父进程,那么,那么当前进程就可以调用setid()接口了,这个进程也就独立出会话之外,成为一个全新的会话,我们称之为 守护进程(精灵进程)!使用类似一下代码:
if(fork() > 0) exit(0);
setsid();
当然,如果你嫌麻烦,大可不必写长点的代码,因为Linux早就给我们想好了,给我们提供了一个 Daemon()
接口:
- nochdir 参数:是否更改当前进程的工作目录。如果更改,守护进程的目录就会切换为根目录,如果不更改,则在启动时的路径下。
- nocliose参数:是否需要进行输入输出的处理。
Linux每个终端下都会存在一个null文件:/dev/null
,如果去读取这个文件,文件内是没有任何内容的,如果对该文件进行写,同样也不会保存任何信息,而是立刻丢弃。我们知道,当我们创建了守护进程,也就意味着脱离了原本的会话,所以也就没有原本的终端文件了,而如果我们要使网络计算器变为守护进程,而网络计算器中存在大量的IO操作,为了避免因为没有对应的终端文件进行IO而出错,我们可以将 0,1,2三个文件描述符全部重定向到 /dev/null 当中。
#include <iostream>
#include <unistd.h>int main()
{std::cout << "Pid is: " << getpid() << std::endl;sleep(1);daemon(0, 0);while(true){std::cout << "hello test" << std::endl;sleep(1);}return 0;
}
以上是一个简单的测试样例,daemon内部会自动的fork并且退出父进程:
经过测试我们可以看到,hello.exe 的TTY,也就是终端文件变成了 “?”, 也就表示已经不属于当前的会话了,而SID同样与当前进程的SID不同,并且SID为守护进程的pid。如果我们查看守护进程的工作目录:
可以看到,守护进程当前工作目录实际上就是在根目录,如果我们同时查看该守护进程的文件fd就会发现:
由此可见,daemon接口的两个参数实际上是bool值类型的,第一个参数表示是否更改工作目录,第二个参数表示是否更改重定向,如果我们把daemon参数设置为daemon(0, 0):
将daemon参数设置为(1, 1)就会导致我们输出的内容还是在上一个会话下,并且Ctrl C
也无法终止进程(可使用 kill -9 process_pid 杀死进程),当我们查询进程工作目录时,也能发现其在当前的工作目录下,而fd也指向了第一个终端文件。
所以一般情况下,我们直接调用 daemon(0, 0)即可,但是我们网络版计算器不仅仅有许多的IO,还写了很多很重要的日志信息啊,这么设置守护进程,我们就无法在终端上看到日志信息了,不用担心,因为早在编写日志之初,我们就已经给日志设置为两个选择,1. 将信息打印到显示器上。2. 将日志信息打印到终端文件上。我们可以将其打印到日志文件当中:
随后启动服务器,将其变为一个守护进程,然后启动一个客户端连接服务器端:
我们之前定义的文件路径就是在当前目录下,而我们创建了守护进程,并且将工作目录改为了根目录,所以我们的log.txt文件只能出现在根目录了。
以上就是网络版计算器实现的全过程了,如果这三篇文章对您有所帮助的话,还望点赞支持~~
相关文章:

【计网】自定义序列化反序列化(三) —— 实现网络版计算器【下】
🌎实现网络版计算器【下】 本次序列化与反序列化所用到的代码,Tcp服务自定义序列化反序列化实现网络版计算器。 文章目录: 实实现网络版计算器【下】 客户端实现 基于守护进程的改写 🚀客户端实现 在这之前,…...
神经网络中的优化方法(一)
目录 摘要Abstract1. 与纯优化的区别1.1 经验风险最小化1.2 代理损失函数1.3 批量算法和小批量算法 2. 神经网络中优化的挑战2.1 病态2.2 局部极小值2.3 高原、鞍点和其他平坦区域2.4 悬崖和梯度爆炸2.5 长期依赖2.6 非精确梯度2.7 局部和全局结构间的弱对应 3. 基本算法3.1 随…...

Linux 计算机网络基础概念
目录 0.前言 1.计算机网络背景 1.1 独立模式 1.2 网络互联 1.3 局域网(Local Area Network,LAN) 1.4 广域网(Wide Area Network,WAN) 2.协议 2.1什么是协议 2.2协议分层和软件分层 2.3 OSI七层网络模型 2.3…...

qt QGraphicsEllipseItem详解
1、概述 QGraphicsEllipseItem是Qt框架中QGraphicsItem的一个子类,它提供了一个可以添加到QGraphicsScene中的椭圆项。QGraphicsEllipseItem表示一个带有填充和轮廓的椭圆,也可以用于表示椭圆段(通过startAngle()和spanAngle()方法ÿ…...
Python websocket
router.websocket(/chat/{flow_id}) 接口代码,并了解其工作流程、涉及的组件以及如何基于此实现你的新 WebSocket 接口。以下内容将分为几个部分进行讲解: 接口整体概述代码逐行解析关键组件和依赖关系如何基于此实现新功能示例:创建一个新的…...

【MySQL-5】MySQL的内置函数
目录 1. 整体学习的思维导图 2. 日期函数 编辑 2.1 current_date() 2.2 current_time() 2.3 current_timestamp() 2.4 date(datetime) 2.5 now() 2.6 date_add() 2.7 date_sub() 2.8 datediff() 2.9 案例 2.9.1 创建一个出生日期登记簿 2.9.2 创建一个留言版 3…...

深度学习笔记之BERT(三)RoBERTa
深度学习笔记之RoBERTa 引言回顾:BERT的预训练策略RoBERTa训练过程分析静态掩码与动态掩码的比较模型输入模式与下一句预测使用大批量进行训练使用Byte-pair Encoding作为子词词元化算法更大的数据集和更多的训练步骤 RoBERTa配置 引言 本节将介绍一种基于 BERT \t…...
C++知识点总结(59):背包型动态规划
背包型动态规划 一、背包 dp1. 01 背包(限量)2. 完全背包(不限量)3. 口诀 二、例题1. 和是质数的子集数2. 黄金的太阳3. 负数子集和4. NASA的⻝物计划 一、背包 dp 1. 01 背包(限量) 假如有这几个物品&am…...

C++:反向迭代器的实现
反向迭代器的实现与 stack 、queue 相似,是通过适配器模式实现的。通过传入不同类型的迭代器来实现其反向迭代器。 正向迭代器中,begin() 指向第一个位置,end() 指向最后一个位置的下一个位置。 代码实现: template<class I…...
webGL入门教程_04vec3、vec4 和齐次坐标总结
vec3、vec4 和齐次坐标总结 1. vec3 和 vec4 1.1 什么是 vec3 和 vec4? vec3: GLSL 中的三维向量类型,包含 3 个浮点数:(x, y, z)。常用于表示三维坐标、RGB 颜色、法线、方向等。 vec4: GLSL 中的四维向量类型&…...

uniapp中父组件数组更新后与页面渲染数组不一致实战记录
简单描述一下业务场景方便理解: 商品设置功能,支持添加多组商品(点击添加按钮进行增加).可以对任意商品进行删除(点击减少按钮对选中的商品设置进行删除). 问题: 正常添加操作后,对已添加的任意商品删除后,控制台打印数组正常.但是与页面显示不一致.已上图为例,选中尾…...
优化 Conda 下载速度:详细的代理配置和网络管理策略
优化 Conda 下载速度:详细的代理配置和网络管理策略 为了彻底解决使用 Conda 下载 PyTorch 时遇到的速度问题,并确保下载过程稳定可靠,这需要一个详细、综合的技术方案。让我们更深入地分析问题原因,然后详尽地解释采取的解决策略…...

服务器遭受DDoS攻击后如何恢复运行?
当服务器遭受 DDoS(分布式拒绝服务)攻击 后,恢复运行需要快速采取应急措施来缓解攻击影响,并在恢复后加强防护以减少未来攻击的风险。以下是详细的分步指南: 一、应急处理步骤 1. 确认服务器是否正在遭受 DDoS 攻击 …...

MFC音视频播放器-支持电子放大等功能
前言 本播放器在VS2019下开发,使用ffmpegD3D实现视频播放渲染功能。同时本播放器支持录像功能、截图功能、音视频播放功能、码流信息显示、电子放大功能等。D3D的渲染同时支持surface和texture两种方式,电子放大功能是在D3D Texture方式下进行实现。以下…...
c语言编程1.17蓝桥杯历届试题-回文数字
题目描述 观察数字:12321,123321 都有一个共同的特征,无论从左到右读还是从右向左读,都是相同的。这样的数字叫做:回文数字。 本题要求你找到一些5位或6位的十进制数字。满足如下要求: 该数字的各个数位之…...
el-table 纵向 横向 多级表头
<el-table :data"tableData" class"diaTable":span-method"handleSpanMethod"border:header-cell-style"{background:#292929,color:#fff}"><!-- 纵向表头 --><el-table-column label"纵向表头" width"…...

uniapp开发微信小程序笔记8-uniapp使用vant框架
前言:其实用uni-app开发微信小程序的首选不应该是vant,因为vant没有专门给uni-app设置专栏,可以看到目前Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。 但是vant的优…...
分布式项目使用Redis实现数据库对象自增主键ID
hello。大家好,我是灰小猿,一个超会写bug的程序猿! 在分布式项目中,数据表的主键ID一般可能存在于UUID或自增ID这两种形式,UUID好理解而且实现起来也最容易,但是缺点就是数据表中的主键ID是32位的字符串&a…...
npm-运行项目报错:A complete log of this run can be found .......npm-cache_logs\
1.问题 没有找到对应的某种依赖,node_modules出现问题。 2.解决 (1)查看对应依赖是否引入或者是由于合并分支错误 引入js或依赖不存在。谨慎删除依赖包 (2)查找对应引入依赖进行安装最后解决方法-删除依赖包清除缓存 npm cache clean --force (2)重新向同事引入…...

SolarCube: 高分辨率太阳辐照预测基准数据集
太阳能作为清洁能源在减缓气候变化中的作用日益凸显,其稳定的供应对电网管理至关重要。然而,太阳辐照受云层和天气变化的影响波动较大,给光伏电力的管理带来挑战,尤其是在调度、储能和备用系统管理方面。因此,精确的太…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

微信小程序 - 手机震动
一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注:文档 https://developers.weixin.qq…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...

Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...

均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...