基于C++/winsock2,进行最小网元设计(上)

简介: 基于C++/winsock2,进行最小网元设计

代码:https://download.csdn.net/download/weixin_55771290/87428996


一、整体架构


1.1 网络拓扑


f2db6510d5e79071011afcf54bc0c1e5.png


我们的网络拓扑模型共设有 7 个网元:1 台路由器、2 台交换机、4 台主机。各网元间形成树形拓扑,通过课程提供的物理层模拟软件进行互联。


主机的网元分为三层,分别是应用层、网络层与物理层;而路由器与交换机,由于不需要与用户进行 I/O 操作,不设应用层。各层间通过(手动或自动)设定的端口进行通信。


网元各层的主要功能如下:


  • 应用层
  • 决定网元模式
  • 信息 I/O
  • 编解码
  • 网络层(主机)
  • 帧同步与定位
  • 地址读写
  • 序号读写
  • 差错检测
  • 差错控制
  • 流量控制
  • 网络层(交换机)
  • 监听各端口消息
  • 维护端口地址表
  • 多主机信息交换
  • 物理层
  • 连接起各网元
  • 模拟误码
  • 模拟 MTU
  • 添加时钟信号等冗余位


1.2 帧结构


在我们的设计中,网元间以帧为单位交换信息;帧内除了用户发送的数据,还有网络层添加的各种控制信息,用于实现差控、流控、判收等功能。


d5ab0175dd992dbebfcc07e8ea6bf582.png


上图是我们组设计的帧结构。它包括以下这几部分:

  • 帧头、帧尾(8 位)
  • 源地址、目的地址(16 位)
  • 帧序号(8 位)
  • 数据(32 位,只能少不能多)
  • 校验和(16 位)
  • 冗余(位数不等,帧同步的副产物)

具体每一部分的功能、原理与实现见第 3 章。

1.3 代码目录结构

程序使用 C++ 编写,网元的每一层是一个 .cpp 文件,交换机、路由器的网络层单独编写一个 .cpp 文件;另外编写有一些封装类、函数的头文件、源文件。



c2bffbe6343903657a6a2bdd38fd9e2f.png

二、应用层


在整个网元中,应用层主要承担着三部分职责:

  • 决定网元模式
  • 控制整个网元处于接收还是发送模式。
  • 信息 I/O
  • 发送端:读取用户想传输的消息;
  • 接收端:输出用户可辨识的消息。
  • 编解码
  • 发送端:Unicode 字符 →01 字符串;
  • 接收端:01 字符串 →Unicode 字符。

下面将分别展示这三种功能。


2.1 决定网元模式


由于技术限制,本项目中的网元是半双工模式,即同时只能处于收/发状态中的一种。这一选择将通过用户手动输入来激活,然后应用层负责将用户的选择通知到整个网元。整体的逻辑如下。

while(true){if(mode==RECV){// 网元成为接收端。
}elseif(mode==UNICAST||mode==BROADCAST){// 网元成为发送端。
}elseif(mode==QUIT){// 退出程序。
}else{// 无效选项。
}}


2.2 信息 I/O


为了操作的便利,本项目中将使用 string 类型,进行绝大部分字符操作。相应地,信息的 I/O 只需要调用 cincout 就能够实现。


2.3 编解码


由于项目需要提供对中文 I/O 的支持,所以显然 ASCII 码无法满足项目的需求,而是需要针对 Unicode 字符设计编解码方案。方案如下图所示:


8f3660e865b0b973c38fc116ac45402d.png


其中,MultiByteToWideChar() 和 WideCharToMultiByte() 来自 windows.h 头文件。在实际中,一个 Unicode 字符可以占到三个字节,但本项目的需求没有那么高,两个字节就能够实现需求,即一个 Unicode 字符对应 16 位二进制数。


具体代码可以在 include/coding.cpp 中找到,该头文件留下 4 个 API 供应用层(与其他层)调用:


  • decToBin():将十进制数转化为 01 字符串;
  • binToDec():将 01 字符串转化为十进制数;
  • encode():将可读字符串编码为 01 字符串;
  • decode():将 01 字符串解码为可读字符串。


2.4 代码框架


细化 2.1 节的逻辑后,我们很容易得出应用层的代码框架:

intmain(intargc,char*argv[]){// 变量、网络库与套接字的初始化。
while(true){// 选择当前模式。
if(mode==RECV){// 通知网络层正在接收。
// 从网络层接收消息。
}elseif(mode==UNICAST||mode==BROADCAST){// 通知网络层正在发送。
if(mode==UNICAST){// 如果是单播,还需要额外输入目标端口。
}// 通知要发的消息。
}elseif(mode==QUIT){// 通知下层退出。
break;}else{// 无效选项,报错。
}}}


2.5 阶段一调试


为了给接下来的阶段做铺垫,我们需要先写两个简单的应用层,对网元间通信的方式、网元工作的模式有一定的理解。对于项目指导书中的需求,下面是我们测试的结果。


项目需求: 客户端定时每 500ms 向服务器发送一个随机整数,范围在 1~500 之间; 服务器每收到一份数据也同时产生随机整数与收到的数据相加,只有在结果大于 100 时才会把计算结果返回给客户端,而客户端收到超出 100 的结果则立即产生一个新的数据,而不是在间隔 500ms 后。 客户机需要产生 20 份数据,如果有超过 100 的结果,总运行时间应接近 10-N*0.5,N 为超过 100 的结果的数量。


24683c049b4d6df99c0f8a67d0dc5b83.png


可以看到,服务端与客户端之间能够进行稳定的通信,客户端通过 select() 实现了超时的判断,实际运行时间与预期时间(10-N*0.5)相符。


通过这个程序,我们了解了控制超时的两种方法:setsockopt() 和 select(),同时也知道如何基于不同事件做出不同的响应,对网元间通信的形式、时序等有了进一步了解。


三、网络层(主机)


在整个网元中,网络层(主机)的功能最多、最重要,主要分为六部分:

  • 帧同步与定位
  • 发送端:让接收端在杂乱的 01 序列中,找到有用的信息。
  • 地址读写
  • 发送端:写入源、目的地址,用于交换、路由的实现;
  • 接收端:读取源、目的地址,知道信息从哪来、是不是给自己的。
  • 序号读写
  • 双端:防止传送时的帧间乱序,也用于差错控制协议的实现;
  • 差错检测
  • 接收端:检查信息有没有传错,如果出错就要求重传。
  • 差错控制
  • 发送端:如何实现某一帧的重传;
  • 接收端:如何让发送端知道要不要重传。
  • 流量控制
  • 双端:防止自己发得太快,导致网络来不及处理、对方来不及读取……等后果。

下面我们将先展示帧的结构,然后分别展示这六种功能。

3.1 帧的结构

为了方便对帧的各部分操作、解析 01 字符串为帧、转换帧为 01 字符串,我们利用 C++“面向对象”的特点,将帧作为一个类。

帧各部分的数据,作为帧的私有属性存储;对帧的操作,作为帧的公共函数绑定。下面是这个类的结构:

classFrame{private:unsignedshortsrcPort;unsignedshortseq;stringdata;unsignedshortdstPort;unsignedshortchecksum;boolverified;stringcheckTarget;stringextractMessage(stringraw);staticstringtransform(stringmessage);staticunsignedshortgenerateChecksum(stringmessage);staticstringaddLocator(stringmessage);public:Frame();Frame(stringraw);Frame(unsignedshortsrcPort,unsignedshortseq,stringdata,unsignedshortdstPort);~Frame();unsignedshortgetSrcPort();unsignedshortgetSeq();stringgetData();unsignedshortgetDstPort();boolisVerified();stringstringify();staticintcalcTotal(intmessageLen);};

我们需要额外关注这两个函数:


Frame(string raw):直接把 01 字符串解析为帧。

stringify():直接把帧转换成 01 字符串。


它们高度的封装性与实用性,使得网络层代码的描述性、可读性变得更强,逻辑也更加清晰。具体的代码可以在 include/frame.cpp 中找到。


3.2 帧同步与定位

采用面向位的首尾定界法

3.2.1 基本原理

  • 发送端:变换,添加帧头帧尾。


在一帧的首尾加上 0111 1110,以标识帧的始末位置;

帧内的信息也有可能出现 0111 1110 的序列,所以为了防止接收端把帧内信息误当作帧尾,发送端还要在帧内的每个 11111 后面插一个 0,以免帧内出现 0111 1110 子序列。

  • 接收端:找到帧头,反变换。
  1. 在物理层收到的乱码中,找到帧头 0111 1110,然后把帧头剥落;
  2. 对于接下来出现的每个 11111 子序列:
  1. 如果接下来出现的是 0,那这个 0 肯定是发送端插的,删掉还原。
  2. 如果接下来出现的是 1,那这就是帧尾 0111 1110。(因为发送方已经保证了帧内不可能出现连续 6 个 1。)

3.2.2 代码实现

我们主要基于 KMP 算法进行子串定位,然后封装了下面三个函数实现帧同步与定位功能:

  • addLocator():实现上述发送端的任务 1;
  • transform():实现上述发送端的任务 2;
  • extractMessage():实现上述接收端的任务 1、2。

具体的代码可以在 include/frame.cppFrame 类中找到。

3.3 地址读写

采用 16 位二进制数标识地址。

3.3.1 取 16 位的原因

由于本项目的网元间通信只在本机(127.0.0.1)实现,所以只需要封装源与目的地的端口即可。又因为端口范围是 0~65535,所以每个端口需要用 16 位二进制表示。

3.3.2 代码实现

发送端只需要使用简单的字符串拼接,即可把地址写入帧;接收端也只需要用 string 类的 substr() 方法,就可以提取地址信息。不再展开叙述。

3.4 序号读写

采用 8 位二进制数标识序号。

3.4.1 取 8 位的原因

项目需求提出,传输数据上限约 50 个字符;又根据 2.3 节得出的结论:一个字符为 16 位,所以一段消息最多有 800 位。


一帧最多传输 32 位数据,所以一段消息最多要用 25 帧,才能传输完毕。


又为了校验和的产生方便(见 3.5 节),序号位数需要是 8 的倍数——最少就是 8 位(范围 0~255),已经有充裕的空间标识每一帧。综上,需为序号分配 8 位的空间。


3.4.2 代码实现

序号读写与地址读写相似,只需要简单的拼接和 substr() 即可实现。不再展开叙述。

3.5 差错检测

采用 16 位校验和,不纠错。

3.5.1 基本原理


  • 发送端:产生校验和。
  1. 将前面的源地址、序号、数据、目的地址这四部分的 01 序列拼在一起,每 8 位视作一个整数;
  2. 全部加起来,得到一个整数;
  3. 再变成 01 序列,作为校验和。


  • 接收端:检验校验和。
  1. 提取出源地址、序号、数据、目的地址这四部分信息;
  2. 使用与发送端同样的方法加和;
  3. 与校验和比较是否相同,相同即验证通过。

3.5.2 视每 8 位为一个整数的原因

  1. 前四部分最多有 16+8+32+16=72 位;
  2. 如果使用经典的 Checksum 生成方法,即视 16 位为一个整数,首先 72 无法整除,带来额外麻烦;其次,得出的和有可能超过 65535,校验和不止 16 位,占用更大空间;
  3. 而如果视 8 位为一个整数,首先 72 能够整除,方便程序实现;其次,和最多只有(72÷8)×255=2295,16 位能够轻松表示。


3.5.3 代码实现

我们封装了函数 generateChecksum(),实现了对任意(长度为 8 的倍数的)01 字符串的校验码生成。

具体的代码可以在 include/frame.cppFrame 类中找到。

3.6 差错控制

采用停等协议

3.6.1 基本原理


  1. 发送端:发送了一帧消息,等待接收端回复;
  2. 接收端:
  3. 如果超时,则回复 NAK
  4. 如果收到了,但是重复了,则丢弃并回复 ACK
  5. 如果收到了,并且校验通过,则回复 ACK
  6. 如果收到了,但是校验失败,则回复 NAK
  7. 发送端:
  8. 如果超时,则重传这一帧。
  9. 如果收到了 ACK,则继续发下一帧;
  10. 如果收到了 NAK,则重传这一帧;
  1. 如果收到的既不是 ACK 也不是 NAK,则重传这一帧;
  2. 回到第 1 步,直到传完所有帧。

3.5.2 采用停等协议的原因

  • 可以顺便控制流量:发送端需要等回复,所以不会发得太快;
  • 编程难度大大降低:只需要实现简单时序逻辑。

3.6.3 代码实现

根据 3.6.1 节所展示的时序,我们可以搭建出双端代码差错控制协议的框架:

注:本处代码暂不考虑广播与单播判收。
  • 发送端


for(intframe=0;frame<=sendTotal;){// 发送一帧。
// 接收对方的回复。
if(timeout){// 重传。
continue;}if(response==ACK){// 可以发送下一帧。
++frame;}elseif(response==NAK){// 重传。
}else{// 重传。
}}
  • 接收端
for(intframe=0;frame<=recvTotal;){// 接收一帧。
if(timeout){// 回复NAK。
continue;}// 检查目标端口。
if(notForMe){// 既不回复也不接收。
continue;}// 检查序号。
if(isRepeat){// 回复ACK但不接收。
continue;}// 检查校验和。
if(!isVerified){// 回复NAK且不接收。
continue;}// 接收这一帧。
// 回复ACK。
++frame;}


相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
编解码 网络安全 网络架构
基于C++/winsock2,进行最小网元设计(下)
基于C++/winsock2,进行最小网元设计(下)
178 0
基于C++/winsock2,进行最小网元设计(下)
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
115 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
191 0
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
243 12
|
7月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
143 16
|
8月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
7月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
7月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
7月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
410 6

热门文章

最新文章