【计网】自定义序列化反序列化(三) —— 实现网络版计算器【下】
🌎实现网络版计算器【下】
本次序列化与反序列化所用到的代码,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: 高分辨率太阳辐照预测基准数据集
太阳能作为清洁能源在减缓气候变化中的作用日益凸显,其稳定的供应对电网管理至关重要。然而,太阳辐照受云层和天气变化的影响波动较大,给光伏电力的管理带来挑战,尤其是在调度、储能和备用系统管理方面。因此,精确的太…...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...
无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...
