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

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: 基于C++/winsock2,进行最小网元设计(下)

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;}

3.7 流量控制

采用 Sleep() 函数。

3.7.1 基本原理

如果发送端发的速度过快,那么有可能导致:


  • 发送端口来不及发;
  • 网络来不及传;
  • 接收端口来不及收;
  • 接收端来不及处理。
  • ……

所以,在调用 sendto() 函数前,让程序先睡眠适当的时间,就可以做到:等上一波信息完全发出去之后,再发这一波信息。

3.7.2 代码实现

只需要在 sendto() 的上一行调用 Sleep() 即可。

这一操作被封装在了各层对应的 Socket 类内,具体的代码可以在 include/socket.cpp 中找到。

3.8 代码框架

将以上所有的功能配合起来,再对广播模式做出一些适配,我们就可以得到网络层的代码框架:

intmain(intargc,char*argv[]){// 变量、网络库与套接字的初始化。
while(true){// 上层通知当前模式。
if(mode==RECV){for(intframe=0;frame<=recvTotal;){// 接收一帧。
if(timeout){// 如果超时没收到消息,回复NAK。
continue;}// 检查目标端口。
if(notForMe){// 如果发来的帧不是给自己的,既不回复也不接收。
continue;}// 检查序号。
if(isRepeat){// 如果重复了,回复ACK但不接收。
continue;}// 检查校验和。
if(!isVerified){// 如果校验失败,回复NAK且不接收。
continue;}// 接收这一帧。
// 回复ACK。
++frame;}// 把拼接完的消息交给应用层。
}elseif(mode==UNICAST||mode==BROADCAST){// 确定目标端口。
// 确定要发的消息。
// 计算要分多少帧。
// 逐帧封装。
for(intframe=0;frame<=sendTotal;){// 发送一帧。
// 接收对方的回复。
// 确定要收几次回复。
for(inti=0;i<recvNum;i++){if(timeout){// 如果超时没收到回复,重传。
continue;}if(response==ACK){// 如果收到了ACK,可以发送下一帧。
++ackTimes;}elseif(response==NAK){// 如果收到了NAK,重传。
}else{// 如果收到了其它信息,重传。
}}// 如果每个接收端都ACK了,就可以发下一帧。
if(ackTimes==recvNum){++frame;}}// 全部发完,封装的帧可以丢弃。
}elseif(mode==QUIT){break;}}}

3.9 阶段二调试


根据上面的代码框架,我们在阶段二写出了应用层与网络层,并使用物理层模拟软件模拟了信道,在两个网元间进行了测试,结果如下。


3.9.1 Unicode 字符的 I/O

187ad7b3fbb80f3cd00ef134120ab6b3.png


在本测试中,用户发送的字符串“你好,test.”中同时包含了中文、英文、全半角符号。可以看到,双方可以完成正常的信息收发,应用层能够正确编解码,网络层也能够逐帧发送与确认。


3.9.2 差错的检测与重传


37b719dd7d8e12c52f16b699726a9592.png


在本测试中,我们设置物理层误码率为十万分之 1000,即 1%。通过网络层多次的检验、回复、重传,应用层最终能够呈现出正确、完整的字符。并且误码率还能够进一步增大。


四、网络层(交换机)


在网络拓扑中,交换机负责在多个主机间交换信息,从而让广播成为可能,同时也减少了 P2P 通路的数量。它的功能主要分为以下三部分:

  • 监听各端口消息
  • 时刻注意有没有端口发来消息。
  • 维护端口地址表
  • 维护本地端口-远程端口的对照表,并通过收发信息时截获端口号进行学习。
  • 多主机信息交换
  • 找到到达目的地的路径,依此在不同网元间转发信息。

下面我们将分别展示这两种功能。


4.1 监听各端口消息

由于交换机需要同时管辖多个物理层,而且需要同时完成收发,所以不能用主机网元的半双工模式。我们考虑采用 select() 方法,轮流监听各个端口是否有消息到达。整体的逻辑如下。

for(inti=0;;i=(i+1)%num){// 使用select()检查端口i的可读性。
if(!readable){// 如果端口i不可读,则检查下一个。
continue;}// 如果端口i可读,就读取信息并进行转发。
}


4.2 维护端口地址表


对于主机而言,它的信息只有一条路可走——就是发到自己的物理层,然后交给交换机。但对于交换机而言,它的信息有不止一条路可走。直接广播给所有端口然后让它们自行判收,显然是浪费信道资源的一种做法,我们更希望交换机自己能够记住,发给谁的信息要走哪个端口。


这就需要它内部维护一张对照表,将本地自己的物理层端口与远程其他主机的应用层端口联系起来。我们使用 map<unsigned short, unsigned short> 类型对此进行管理。整体的逻辑如下。


// 获取截获的帧的源与目的地址。
// 查表获取这个本地端口有没有注册过源地址。
if(srcPortNotInTable){// 如果表里没有这个源地址,就将这个源地址和本地端口联系起来。
}// 查表获取目的地址对应的本地端口。
if(dstPortNotInTable){// 如果表里没有这个目的地址,就向所有端口广播。下一轮回复的时候就能学习了。
}

4.3 多主机信息交换


这是交换机最基础、最本质的功能。它需要判断消息发送的形式(单播或广播),然后据此采取相应的行动。整体的逻辑如下。

if(isBroadcast||portNotFound){// 如果是广播,或者没找到目的地址对应的本地端口,就给所有端口发消息。
}else{// 如果是单播,就直接发送。
}

4.4 代码框架


将以上的三部分结合起来,就是网络层(交换机)的代码框架。


intmain(intargc,charconst*argv[]){// 变量、网络库与套接字的初始化。
for(inti=0;;i=(i+1)%phyPortNum){// 如果该端口不可读,则检查下一个。
if(!readable){continue;}// 如果可读,就读取消息。
// 获取消息的源与目的端口。
// 反向学习源端口。
// 检索应该发到哪个端口。
// 判断要单播还是广播。
if(isBroadcast||portNotFound){// 给所有端口发消息。
}else{// 给对应端口发消息。
}}}

4.5 阶段三调试

4.5.1 单播的支持

82e8df4fa28b2688b9458f7b6b2b3a50.png


在测试中,我们从 11300 端口向 12300 单播“hello”。可以看到,交换机能够正确的学习并更新端口地址表,并充当两主机发送、回复、重传的桥梁,消息最终完好地传递到了 12300 端口。


4.5.2 广播的支持


3bcbeeaa2e69be8e928d7be175adb0ee.png

在测试中,我们从 12300 端口向所有端口广播“helloworld”。可以看到,在发送时,交换机能够正确识别出这是广播消息,并广播给所有剩余端口;在回复时,交换机也能够正确识别出这是单播消息,并单播给 12300 端口。最终,消息完好地传递到了所有端口。


4.5.3 反向学习


  1. 当 11300 端口第一次向交换机的 13101 端口发消息,交换机的表里还没有 13101-11300 的对应关系,于是它自动学习了这组关系;
  2. 当交换机第一次向 12300 端口发消息,它的表里还没有 13102-12300 的对应关系,于是它向所有端口广播了这条消息;
  3. 当 12300 端口第一次向交换机的 13102 端口回复消息,交换机就学习到了 13102-12300 这组关系;
  4. 当交换机向 11300 端口回复消息,此时它已经学会了 13101-11300 的关系,所以回复以单播形式发给 11300 端口。
  5. 此后,所有信息交互都是单播,因为交换机已经把两边与自己的对应关系都学会了。


五、网络层(路由器)


最后,我们将编写路由器程序,并更新物理层配置文件,实现我们的七网元拓扑。


在正常情况下,路由器负责的是 IP 的寻址、分配,提供防火墙,接入互联网等功能,但由于我们的网络拓扑只在本机进行,IP 地址恒为 127.0.0.1,并且也不需要防火墙等服务,所以我们的路由器功能与交换机功能大致类似。


5.1 实体编址

5.1.1 基本原理

当我们的网络拓扑逐渐开始变得复杂,我们需要为我们网络中的每个网元分配明晰合理、规则统一的端口号。

我们编址的规则如下:


  • 第一位:统一为 1
  • 第二位:网元编号,范围为 1-7
  • 第三位:层号,从下往上分别为 1-3
  • 第四、五位:实体号,从 00 开始往上编。

根据这些原则,我们规划出的拓扑端口号如下:


6a03fa3add0527a897e15e0b37bc37b8.png


在数据投递时,用户需要输入目标网元的应用层端口。比如我要从主机 3 向主机 1 发消息,那么我就要输入主机 1 应用层的端口号 11300。


这么设计的原因是想要更贴近实际:例如我想要给某人发 QQ,我希望直接通过备注或昵称,在我的列表里找到他;而不是通过 QQ 号,甚至他在网络上的 IP 地址找到他——后面这些是软件底层逻辑该关心的事情,而不是用户该关心的。


5.1.2 配置文件

为了更快地启动并配置物理层参数,我们根据以上的拓扑在 ne.txt 进行了对应的配置。

各网元分层:

1 PHY0 NET0 APP0
2 PHY0 NET0 APP0
3 PHY0 SWITCH0
  PHY1
  PHY2
4 PHY0 NET0 APP0
5 PHY0 NET0 APP0
6 PHY0 SWITCH0
  PHY1
  PHY2
7 PHY0 ROUTE0
  PHY1

网元间连接:

1. 1,0--3,0
2. 2,0--3,1
3. 4,0--6,0
4. 5,0--6,1
5. 7,0--3,2
6. 7,1--6,2


5.2 路由表


在正常情况下,路由器的路由表结构大致如下:

  • 目的:数据包想要发到哪台主机;
  • 下一站:为了转发,路由器应该把数据包转交给哪台路由器;
  • 出口:为了转交,数据包要从路由器哪个端口出去;
  • 度量:这条路径要经过多少中继路由器。


但由于我们的网络拓扑中只有一台路由器,所以并不存在“下一站”的概念,度量也始终为 0。我们只需要记录,到哪个远程端口的数据包应该走哪个端口即可。

这与交换机的端口地址表逻辑基本一致,我们可以直接挪用,并同时支持反向学习。代码逻辑不再展示。


5.3 批处理文件


加入路由器后,需要运行的 *.exe 文件数量陡增,用户逐个打开并输入端口号显然是费时费力的一件事情。为此,我们编写了三个批处理文件,分别负责项目的一键编译、启动、关闭。

  • OneTouchToCompile.bat:一键编译 src 文件的所有源码。
  • OneTouchToGo.bat:一键启动所有网元;
  • OneTouchToClose.bat:一键关闭所有网元。

这三个文件可以在项目根目录找到,使用时双击即可。


5.4 阶段四调试

在调试过程中,我们发现了一个令人费解的问题:主机发来的消息,在交换机处会以错误的端口号出现。


fb32415fd3fbc189b7ff20861ed382b9.png


如上图,在主机 2 向主机 1 返回 ACK 时,本应该在 13101 端口(主机 2 对应端口)出现的消息,被交换机误认为是从 13100 端口(主机 1 对应端口)传来,导致端口地址表混乱,后续的收发也无法完成。而在阶段三未加入路由器时,我们并没有出现类似的问题。


我们判断,问题应该出现在 select 驱动部分,但我们小组前后对 SwitchSocket 类和 switcher.cpp 的相关部分进行了若干次的逻辑 + 代码的重构,仍未能解决这一问题。


经过对项目代码的多次审视,我们小组认为代码逻辑没有出现问题;恳请老师指点问题所在,我们小组将不胜感激。


六、反思与总结


6.1 项目的缺点与反思


本项目成功实现了阶段一到三的所有基本要求,并完成了阶段四的代码;但在最终的调试中遇到了问题,未能完成阶段四的调试。


除此之外,我们的项目还有一些小缺点:


  1. 在误码率较高、多次重传的情况下,string 类的 substr() 方法偶尔会抛出上溢错误。由于触发 bug 的频率低,目前尚未定位到错误,但不影响正常的收发;
  2. 交换机在执行 select() 时,偶尔会将没有信息的端口误认为有信息可读。目前暂且通过“判断读入字节数大于 0”解决了该问题,但这个方案并不漂亮;
  3. 只支持中英文与标点的传输,但不支持图片传输。这可以通过编写特定的编解码函数,将图片的每个像素点的 RGBA 值转化为 01 序列,再进行传输,但由于时间与精力有限,未能实现支持。


6.2 总结与心得体会


该项目是课程四个项目中最难的一个,它涵盖了课程中的许多知识点:定位、检错、差控、流控、共享、交换、路由、转发、广播……但相应地,它也是给我们小组最多收获的一个。


通过这一项目,我们小组真正地将课堂所学的内容,应用到了实际的编程当中,并且也对这门课程的授课大纲有了更深层次的把握。我们不再只是知识的接收者,而是成为设计者,通过实操真正理解了为什么、怎么样;也因此,我们的专业知识水平、程序设计水平、团队合作水平得到了极大的提高。


虽然我们小组并没有能够将代码写得十全十美、令人满意,但这一项目给我们的收获——我们认为——是远远超过代码本身的。


感谢老师们在我们组项目推进的过程中,提供的悉心教导与陪伴!


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
存储 编解码 算法
基于C++/winsock2,进行最小网元设计(上)
基于C++/winsock2,进行最小网元设计
139 1
基于C++/winsock2,进行最小网元设计(上)
|
5天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
25 5
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
36 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
24 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1