【计算机网络】网络版本计算器

2024-08-21 03:36

本文主要是介绍【计算机网络】网络版本计算器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

此前我们关于TCP协议一直写的都是直接recv或者read,有了字节流的概念后,我们知道这样直接读可能会出错,所以我们如何进行分割完整报文?这就需要报头来解决了!

但当前我们先不谈这个话题,先从头开始。

将会着重理解OSI 7层模型中传输层向上的3层,并编码进行解释。

而恰好tcp/ip模型是4层(或5层),将OSI上三层统一压缩为1层应用层了,这究竟又有什么关系呢?
在这里插入图片描述
我们实际编程中也正是按照这种模式进行编写的。

目录

  • 1. 服务端
    • 1.1 会话层
    • 1.2 表示层
    • 1.3 应用层
  • 2. 客户端
    • 2.1表示层
  • 3. 完整代码:

1. 服务端

1.1 会话层

会话层是一个什么意思?
通俗理解就是建立连接与断开连接,也就是connect与accept

我们都是在server的hpp文件中进行的:

下段代码是一个大概的流程,这也就是编码实现会话层

int fd = accept...IO_service(fd等需要的参数...)close(fd)

1.2 表示层

这段话确实抽象
在这里插入图片描述
通俗理解就是:两个通信的主机按照一定的格式进行传输信息:即按照相同的请求协议与响应协议进行转化(序列化和反序列化)传输数据。

在之前的基于tcp的网络服务程序中,我们表面没有体现出这个,但实际上我们传输的协议就是传输字符串,这也是我们约定好的。

这层也是我们今天的重点。

由于我们当前是基于结构体传输(请求协议与响应协议),但是由于技术原因与业务原因导致直接使用结构体传输会导致各种各样的问题,所以我们序列化为固定格式的字符串进行传输,在反序列为你需要的协议格式进行操作。这是我们在应用层自定义协议就已经说过的了。
但因为是字节流的原因,所以recv或read到的字符串不一定是完整的请求,因此需要添加报头进行解决。

我们先来解决序列化与反序列化的问题。
其中序列化我们可以手写,也可以借助各种各样的库文件进行操作,这里我们选择使用json,最主要的原因还是因为可视化:


关于json我们可以大概的了解一下,熟悉一下接口即可。

class Request
{
public:Request(int x, int y, char oper): _x(x),_y(y),_oper(oper){}Request(){}void Serialization(std::string *out){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StyledWriter writer;// Json::FastWriter writer;*out = writer.write(root);}void Deserialization(const std::string &in){Json::Reader reader;Json::Value root;reader.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}~Request(){}public:int _x;int _y;int _oper;
};int main()
{Request req(1, 1, '*');std::string str;req.Serialization(&str);std::cout << str << std::endl;Request req1;req.Deserialization(str);req.Print();return 0;
}

注意由于json是第三方库,记得编译时-ljsoncpp
验证:
在这里插入图片描述


尽管我们现在是在进行序列化与反序列化,但是在序列化与反序列化前总得有请求请求协议与响应协议吧。


class Request
{
public:Request(int x, int y, char oper): _x(x),_y(y),_oper(oper){}Request(){}~Request(){}public:int _x;int _y;int _oper;
};class Response
{
public:Response(): _code(0),_desc("sucess"){}~Response(){}public:int _result;int _code; // 0:sucess, 1:div zero, 2:mod zero, 3:invalid operstd::string _desc;
};

上段代码就是两个协议的基本内容。

因此我们现在即可构建请求协议与响应协议的序列化与反序列化,没错,每种协议都需要构建序列化与反序列化:
当客户端构建数据构需要序列化再传输,服务端接收后再反序列化;
服务端处理完数据后再将响应协议对象序列化传输,客户端再反序列化得到结果。

这张图就很形象的展示了过程。
在这里插入图片描述
但是我们还需要解决如何获得的是一个完整的请求的问题,我们已经说过解决方案了,那就是添加报头,于是我们进一步完善协议。

那么添加的报头是怎样的格式?

"len"\r\n{json串}\r\n

这种形式是非常通用的,我们在HTTP协议中也可以看到这种形式的影子。

现在解释一下参数
len就是json串的长度,\r\n本质上就是换行,这样的健壮性更强(\r是回退到初始行,\n换行,但现在我们的\n基本上都包含了换行到新行的开头的功能了。)

现在解释一下为什么这么做:

// "le --> 残缺报文
// "len"\r\n{json} --> 残缺报文
// "len"\r\n{json}\r\n --> 完整报文
// "len"\r\n{json}\r\n"len"\r\n{jso --> 冗余报文
// "len"\r\n{json}\r\n"len"\r\n{json}\r\n"len"\r\n{json}\r\n --> 冗余报文

因为面向字节流,所以我们有可能得到的数据是以上样子,当我们这样设计报头时,不论何种情况都可以处理。
假设是第一种情况:我们先find \r\n,若是没有则说明当前是不完整的报文,继续recv即可。
若是第二种:我们由于find到了\r\b,所以就可得知json串的具体长度,根据具体长度得到是否为完整的报文。
若是第四/五种:我们直接截取最前方的完整报文即可。

故此时我们即可设计添加报头。

// 添加报头
std::string sep = "\r\n";std::string EnHeader(const std::string &packagestream)
{int len = packagestream.size();std::string ret = std::to_string(len);return ret + sep + packagestream + sep;
}// 注意这里我们传参是非const,原因在于当得到不完整报文返回时,还能续接。(具体可以在完整代码中体现)
std::string DeHeader(std::string &packagestream)
{// 还没有读到lenauto pos = packagestream.find(sep);if (pos == std::string::npos){return {}; }// 检查是否为一个完整的json串int len = std::stoi(packagestream.substr(0, pos));int total = pos + len + 2 * sep.size();if (total > packagestream.size()){return {};}// 至少有一个完整的json串std::string ret = packagestream.substr(pos + sep.size(), len);packagestream = packagestream.erase(0, total);return ret;
}

如此准备工作便都做好了。

可以进行传输与接收了。

while (true)
{int n = socket->Recv(&messagequeue);if (n <= 0){break;}// 2. 去报头std::string ret = DeHeader(messagequeue);if (ret.size() == 0){continue;}// 3. 一个完整的报文,进行反序列化// 只是利用工厂模式造了一个请求协议智能指针,无需重点关注,重点是进行序列化std::shared_ptr<Request> req = Bulider::GetReq();req->Deserialization(ret);// 4. 执行业务std::shared_ptr<Response> resp = _func(req);// 5. 序列化std::string jsonmessage;resp->Serialization(&jsonmessage);// 6. 添加报头jsonmessage = EnHeader(jsonmessage);// 7. 发送数据n = socket->Send(jsonmessage);if (n < 0){break;}
}

1.3 应用层

应用层就是处理我们的业务的,
我们上段代码的第四步就是应用层。

这里根据我们制作的网络计算机设计对应的业务即可。

class Calculator
{
public:Calculator(){}std::shared_ptr<Response> calculate(std::shared_ptr<Request> req){std::shared_ptr<Response> resp = std::make_shared<Response>();std::cout << req->_x << req->_oper << req->_y << std::endl;switch (req->_oper){case '+':resp->_result = req->_x + req->_y;break;case '-':resp->_result = req->_x - req->_y;break;case '*':resp->_result = req->_x * req->_y;break;case '/':{if (req->_y == 0){resp->_code = 1;resp->_desc = "div zero";}else{resp->_result = req->_x / req->_y;}}break;case '%':{if (req->_y == 0){resp->_code = 2;resp->_desc = "mod zero";}else{resp->_result = req->_x % req->_y;}}break;default:{resp->_code = 3;resp->_desc = "illegal operation";}break;}return resp;}~Calculator(){}
};

最后将3层整合在一起即可,这些便都是套路了,在完整代码中即可看到。

2. 客户端

本质上与服务端是很相似的,只是那几个步骤变了变顺序而已。

2.1表示层

while (true)
{// 构建数据std::shared_ptr<Request> req = Bulider::GetReq();req->_x = rand() % 10;req->_y = rand() % 10;req->_oper = oper[req->_y % oper.size()];// 序列化数据std::string reqstr;req->Serialization(&reqstr);// 添加报头reqstr = EnHeader(reqstr);// 发送数据int n = sockclient->Send(reqstr);// 接收数据std::string recvstr;while (true){n = sockclient->Recv(&recvmessage);if (n < 0){break;}// 去报头recvstr = DeHeader(recvmessage);if (recvstr.size() == 0){continue;}break;}// 反序列化std::shared_ptr<Response> resp = Bulider::GetResp();resp->Deserialization(recvstr);
}

3. 完整代码:

Gitee代码展示。

在这里插入图片描述

完~

这篇关于【计算机网络】网络版本计算器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1092006

相关文章

Python版本与package版本兼容性检查方法总结

《Python版本与package版本兼容性检查方法总结》:本文主要介绍Python版本与package版本兼容性检查方法的相关资料,文中提供四种检查方法,分别是pip查询、conda管理、PyP... 目录引言为什么会出现兼容性问题方法一:用 pip 官方命令查询可用版本方法二:conda 管理包环境方法

Python实现简单封装网络请求的示例详解

《Python实现简单封装网络请求的示例详解》这篇文章主要为大家详细介绍了Python实现简单封装网络请求的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录安装依赖核心功能说明1. 类与方法概览2.NetHelper类初始化参数3.ApiResponse类属性与方法使用实

Python一次性将指定版本所有包上传PyPI镜像解决方案

《Python一次性将指定版本所有包上传PyPI镜像解决方案》本文主要介绍了一个安全、完整、可离线部署的解决方案,用于一次性准备指定Python版本的所有包,然后导出到内网环境,感兴趣的小伙伴可以跟随... 目录为什么需要这个方案完整解决方案1. 项目目录结构2. 创建智能下载脚本3. 创建包清单生成脚本4

Debian 13升级后网络转发等功能异常怎么办? 并非错误而是管理机制变更

《Debian13升级后网络转发等功能异常怎么办?并非错误而是管理机制变更》很多朋友反馈,更新到Debian13后网络转发等功能异常,这并非BUG而是Debian13Trixie调整... 日前 Debian 13 Trixie 发布后已经有众多网友升级到新版本,只不过升级后发现某些功能存在异常,例如网络转

Ubuntu如何升级Python版本

《Ubuntu如何升级Python版本》Ubuntu22.04Docker中,安装Python3.11后,使用update-alternatives设置为默认版本,最后用python3-V验证... 目China编程录问题描述前提环境解决方法总结问题描述Ubuntu22.04系统自带python3.10,想升级

使用Python实现一个简易计算器的新手指南

《使用Python实现一个简易计算器的新手指南》计算器是编程入门的经典项目,它涵盖了变量、输入输出、条件判断等核心编程概念,通过这个小项目,可以快速掌握Python的基础语法,并为后续更复杂的项目打下... 目录准备工作基础概念解析分步实现计算器第一步:获取用户输入第二步:实现基本运算第三步:显示计算结果进

Python开发简易网络服务器的示例详解(新手入门)

《Python开发简易网络服务器的示例详解(新手入门)》网络服务器是互联网基础设施的核心组件,它本质上是一个持续运行的程序,负责监听特定端口,本文将使用Python开发一个简单的网络服务器,感兴趣的小... 目录网络服务器基础概念python内置服务器模块1. HTTP服务器模块2. Socket服务器模块

Go语言网络故障诊断与调试技巧

《Go语言网络故障诊断与调试技巧》在分布式系统和微服务架构的浪潮中,网络编程成为系统性能和可靠性的核心支柱,从高并发的API服务到实时通信应用,网络的稳定性直接影响用户体验,本文面向熟悉Go基本语法和... 目录1. 引言2. Go 语言网络编程的优势与特色2.1 简洁高效的标准库2.2 强大的并发模型2.

更改linux系统的默认Python版本方式

《更改linux系统的默认Python版本方式》通过删除原Python软链接并创建指向python3.6的新链接,可切换系统默认Python版本,需注意版本冲突、环境混乱及维护问题,建议使用pyenv... 目录更改系统的默认python版本软链接软链接的特点创建软链接的命令使用场景注意事项总结更改系统的默

Linux升级或者切换python版本实现方式

《Linux升级或者切换python版本实现方式》本文介绍在Ubuntu/Debian系统升级Python至3.11或更高版本的方法,通过查看版本列表并选择新版本进行全局修改,需注意自动与手动模式的选... 目录升级系统python版本 (适用于全局修改)对于Ubuntu/Debian系统安装后,验证Pyt