随便说说

上一节我们似乎只是简单地说了“Hello“,然后介绍了一丢丢技巧,对NS3这个庞大的世界还没有进行真正的探索。

先前在自己的帖子搭建P2P网络介绍过first脚本,但当时的自己也是一知半解(虽然现在也没好多少),文章里只是简单分析了代码各部分的功能,然后展现了实验结果和工具的使用过程,并没有很细致去学习代码。因此决定带领读者和自己一起动手搭建一个最简单的P2P网络,一起学习NS3的代码逻辑。

顺口一提,关于工具NetAnim、Tracemetrics的使用方法,在之前的文章中有详细的介绍,如有需要可移步~搭建P2P网络

问题来了

现需要创建一个包含两个节点的有线网络,链路层使用点对点协议(Point-To-Point,PPP)进行分组传输,点对点信道要求传输速率5Mbit/s、传播延迟2ms,其中1号节点作为服务器、0号节点作为客户端。

认识头文件

NS3脚本主要以C++语言编写,其头文件的均以:”-module.h”命名,因此根据脚本需要的模块进行引入即可。

那么我们一起来想一下,搭建一个p2p网络需要哪些模块呢?当新手看到这里八成是要在心里骂街,模块那么多那么多,。别急,让我们一起去认识他们。

#include “ns3/core-module.h”:定义了ns3的核心功能:模拟事件、事件调度巴拉巴拉

#include “ns3/network-module.h”:基本网络组件:网络结点、分组和地址

以上两个头文件是所有脚本必须要有的,NS3可是一个模拟器,怎么能少了core~要是连网络节点都无法定义,怎么能继续进行呢” />定义了TCP/IP协议栈

#include “ns3/applications-module.h”:定义了分组收发模型:贪婪模型、ON/OFF模型等

这两个模块不是必需的,但是基本都会用到,毕竟网络和应用的环节还是不可缺少的。

命名空间与日志系统

using namespace ns3:ns3命名空间保护整个ns3源代码,方便项目与非ns3项目隔离与整合

这也意味着如果只引用ns3命名空间时,想要使用标准库函数(cin、cout等)必须使用std::cin…

因此一般习惯性地将标准库命名空间一同加上

using namespace std;

下面定义一个日志组件,名为“My New First”

NS_LOG_COMPONENT_DEFINE(“My New First”);

日志系统对代码调试和了解模拟流程都有着很重要的作用,但目前对这部分了解还不太多,后续会继续学习。

准备阶段

现在让我们进入main,开始正式编写脚本吧

CommandLine cmd;cmd.Parse(argc,argv); 

这里定义了一个命令行变量,其作用是可以通过终端去临时修改一些变量,方便调试

LogComponentEnable("UdpEchoClientApplication",LOG_LEVEL_INFO);LogComponentEnable("UdpEchoServerApplication",LOG_LEVEL_INFO);

这里的两步是为了打印Log组件的信息,分别输出服务器和客户端的信息

准备阶段里设置命令行与日志组件,可能一开始很不好理解,不过随着学习的深入,会越来越明了的。这部分内容推荐大家去阅读马春光先生的教材《ns-3网络模拟器基础及应用》,书中对这部分使用了很多实例带领读者体会他们的功能。

创建网络拓扑吧

NodeContainer nodes;nodes.Create(2);

定义一个结点容器nodes并调用nodes中的创建节点方法Create,指定节点数量2

PointToPointHelper pointToPoint;pointToPoint.SetDeviceAttribute("DataRate",StringValue("5Mbps"));pointToPoint.SetChannelAttribute("Delay",StringValue("2ms"));

定义一个PPP助手类,根据题目要求分别设置设备和信道参数:传输速率5Mbit/s、传播延迟2ms

在这里我们要认识一个概念——助手。

助手类屏蔽了很多实现细节,其可以帮助用户更加简便地在脚本中创建网络拓扑。在本文问题中,PPP助手提供了设置队列类型、设备属性和通道属性的方法,并且可以安装点对点网络设备。还提供了启用pcap输出和ASCII跟踪输出的功能,

现在我们的拓扑中,相当于有两个节点,并通过助手类设置好了信道和设备的参数,那么问题来了,如何把这两个节点连接在这个信道上呢,这就需要一个新成员:设备类(NetDevice)

NetDeviceContainer devices;devices = pointToPoint.Install(nodes);

写到这里时,相信包括我在内的很多童鞋都会有些懵:PPP助手的Install方法帮助节点容器nodes的所有成员安装了网络设备,为什么还要再定义一个网络设备容器devices去接收呢。

这里我们需要看一下源码,在vscode中Ctrl+鼠标点击pointToPoint.Install的Install,跳转到该函数的源码,我们会看到:

NetDeviceContainerPointToPointHelper::Install(Ptr a, Ptr b){NetDeviceContainer container;............return container;}

是否清晰一些?该函数会返回一个网络设备容器类的变量,因此在脚本中使用devices去接收这个函数。如果还是不太理解为什么要单独拎出来一个devices变量的话,没有关系,我们先姑且这样想:目前节点有了,设备呢?助手是可以创建,可是他屏蔽掉了创建的细节,我们需要”看得见的设备“,因此NetDeviceContainer devices便被创建出来了,指不定后续要用它来干点什么呢~

后续的过程会让我们对这个问题更加清晰的,别急~

截至目前,我们的拓扑结构是这样的:

一般来说,NetDevice负责链路层协议、Channel负责物理层协议。至此,我们的拓扑可以完成链路层和物理层的通信。

安装TCP/IP协议栈

InternetStackHelper stack;stack.Install(nodes);

利用协议栈助手定义一个协议栈变量,使用Install函数安装在节点容器中(所有节点都被安装),此时所有节点都有了TCP/IP协议栈

那这里为什么不再用另一个变量去接收呢,源码中InternetStackHelper::Install并没有返回一个别的类型的变量,这怕是最直接的解释了吧。

Ipv4AddressHelper address;address.SetBase("10.1.1.0","255.255.255.0");Ipv4InterfaceContainer interfaces = address.Assign(devices);

用地址助手定义变量,使用函数定义网络的起始地址和掩码,Assign函数返回一个接口容器类型的变量,因此使用一个接口容器接收函数输出。到这里可以去看一下Assign函数的逻辑:

  • Assign函数循环遍历DevicesContainer,
  • 获取设备所属节点编号
  • 检查节点是否存在
  • 然后检查ipv4接口是否存在,不存在则通过ipv4为设备添加接口
  • 然后再接口上部署ipv4地址
  • 最后把地址和接口赋给interface对象

目前位置整个网络的部署情况如下图所示

图中IpL4Protocol涉及到后续传输层的内容,first脚本中并没有体现出来,可以忽略掉,重要的是看懂NS3节点的组成。一层一层,设备上接口,接口上是地址,地址在上就是应用。现在再回头看看刚才关于定义变量接收返回值的疑问,是不是清晰一点。

安装应用

UdpEchoServerHelper echoSever(9);

利用udp服务器助手设置服务器程序属性:侦听9号端口。但这里要注意的是,只是设置了属性,并没有定义一个程序。

ApplicationContainer serverApps = echoSever.Install(nodes.Get(1));

这个逻辑相信大家也慢慢熟悉了,用助手的安装函数给节点1安装服务器应用程序,这个应用程序使用ApplicationContainer类型变量接收,此时真正地将应用程序安装在了节点1上。

serverApps.Start(Seconds(1.0));serverApps.Stop(Seconds(10.0));

设置应用的起止时间,这个清晰易懂~

下一步开始为我们的客户端安装程序:

UdpEchoClientHelper echoClient(interfaces.GetAddress(1),9);

客户端助手在设置程序属性时有些不同,你会发现,明明0号节点时客户端,为什么这里要GetAdress(1), 还要把服务器的9号端口给他呢?这样与直接理解相反的逻辑,我们还是去看下源码吧。

 /** * Create UdpEchoClientHelper which will make life easier for people trying * to set up simulations with echos. Use this variant with addresses that do * not include a port value (e.g., Ipv4Address and Ipv6Address). * * \param ip The IP address of the remote udp echo server * \param port The port number of the remote udp echo server */UdpEchoClientHelper(Address ip, uint16_t port);/** * Create UdpEchoClientHelper which will make life easier for people trying * to set up simulations with echos. Use this variant with addresses that do * include a port value (e.g., InetSocketAddress and Inet6SocketAddress). * * \param addr The address of the remote udp echo server */UdpEchoClientHelper(Address addr);

你会发现源码中有两种定义方式,而其中Address都是指远端服务器的地址,端口port也是服务器的端口。

与服务器相比,客户端得多设置些属性,包括,最大传输的分组数、传输时间间隔、分组大小。

 echoClient.SetAttribute("MaxPackets",UintegerValue(1));echoClient.SetAttribute("Interval",TimeValue(Seconds(1.0)));echoClient.SetAttribute("PacketSize",UintegerValue(1024));

下一步的安装大家应该熟悉一些了,随后就是设置开始和结束时间:

ApplicationContainer clientApps = echoClient.Install(nodes.Get(0));clientApps.Start(Seconds(2.0));clientApps.Stop(Seconds(10.0));

应用安装结束,至此实现的功能为

  • 0号节点作为客户端,再仿真开始后2s开始工作,
  • 向作为服务端的节点1每隔1s发送1个大小为1024的包
  • 服务器从9号端口收到后向客户端返回一个相同大小的包
  • 客户端与服务器在10s后均停止工作

最后别忘了开启和结束模拟:

Simulator::Run();Simulator::Destroy();return 0;

关于输出文件

关于模拟过程中输出的pcap、tr、xml文件,我们在second脚本继续学习,其中一个原因在于,first 脚本只有两个节点,对于文件输出时无法体现不同输出函数的效果。

先前的文章对输出文件的处理有着详细的描述,可供大家参考~

完整代码

把带有注释的代码送给大家~

#include "ns3/core-module.h"// 定义了ns3的核心功能:模拟事件、事件调度巴拉巴拉#include "ns3/network-module.h"// 基本网络组件:网络结点、分组和地址#include "ns3/internet-module.h"// 定义了TCP/IP协议栈#include "ns3/point-to-point-module.h"#include "ns3/applications-module.h"// 定义了分组收发模型:贪婪模型、ON/OFF模型等#include "ns3/netanim-module.h"using namespace ns3;// ns3命名空间保护整个ns3源代码,方便项目与非ns3项目隔离与整合using namespace std;// 标准库函数命名空间:cout、min......NS_LOG_COMPONENT_DEFINE("My New First");// 作为允许在脚本中使用LOG系统的宏定义打印辅助信息,个人理解就是记录报错信息int main(int argc, char *argv[])//从这里开始,后续操作都将在main函数中完成{/********************************准备阶段*************************************/CommandLine cmd;cmd.Parse(argc,argv); // 这里定义了一个命令行变量,其作用是可以通过终端去临时修改一些变量,方便调试Time::SetResolution(Time::NS);// 这里定义了脚本中的最小时间单元 ns 纳秒LogComponentEnable("UdpEchoClientApplication",LOG_LEVEL_INFO);LogComponentEnable("UdpEchoServerApplication",LOG_LEVEL_INFO);// 这里的两步是为了打印Log组件的信息,分别输出服务器和客户端的信息/*****************************准备阶段结束*************************************//*****************************创建网络拓扑*************************************/NodeContainer nodes;nodes.Create(2);//定义一个结点容器nodes//调用nodes中的创建节点方法Create,指定节点数量2PointToPointHelper pointToPoint;//定义一个PPP助手类/*该类的目的是为了简化创建点对点网络的过程。它提供了设置队列类型、设备属性和通道属性的方法,并且可以安装点对点网络设备。此外,它还提供了启用pcap输出和ASCII跟踪输出的功能。*/pointToPoint.SetDeviceAttribute("DataRate",StringValue("5Mbps"));pointToPoint.SetChannelAttribute("Delay",StringValue("2ms"));// 分别设置设备和信道参数NetDeviceContainer devices;devices = pointToPoint.Install(nodes);// pointToPoint成员函数Install(NodeContainer c):安装点对点网络设备并返回一个NetDeviceContainer对象。// 再用刚才创建的NetDeviceContainer容器devices接收,devices作为连接节点和信道的中介// 助手类(helper)会屏蔽很多细节问题pointToPoint.Install(nodes)对nodes容器中的两个节点都安装了netdevice,// 并将两个设备连接在已经设置好参数的信道上,并返回一个netdevicecontainer类/*************************创建网络拓扑结束*************************************//*****************至此两个节点可以在物理层和链路层通信***************************//*************************安装TCP/IP协议栈************************************/InternetStackHelper stack;stack.Install(nodes);// 定义InternetStackHelper助手类,并为nodes容器中的节点安装TCP/IP协议栈Ipv4AddressHelper address;address.SetBase("10.1.1.0","255.255.255.0");// 定义ipv地址助手,设置网络起始地址和掩码Ipv4InterfaceContainer interfaces = address.Assign(devices);// 定义ipv4网络接口助手,接收address.Assign函数返回的interfaceContainer类型变量/*分析源码,Assign函数循环遍历DevicesContainer,获取设备所属节点编号检查节点是否存在,然后检查ipv4接口是否存在,不存在则通过ipv4为设备添加接口然后再接口上部署ipv4地址最后把地址和接口赋给interface对象*/ //至此两个节点的地址分别为10.1.1.1和10.1.1.2/*************************安装TCP/IP协议栈************************************//******************至此两个节点可以运行基于TCP/IP的通信*************************//***************************安装应用程序************************************//*ns3中的应用程序只是模拟分组的发送和接收行为,此脚本中选择UdpEcho程序,后续还有贪婪分组发送等应用程序模型*/UdpEchoServerHelper echoSever(9);// 设置服务器程序属性:侦听9号端口ApplicationContainer serverApps = echoSever.Install(nodes.Get(1));// 给0号 节点安装服务端应用程序serverApps.Start(Seconds(1.0));serverApps.Stop(Seconds(10.0));// 设置服务端开始和结束工作的时间UdpEchoClientHelper echoClient(interfaces.GetAddress(1),9);// 设置客户端程序属性,获取服务端的地址和端口// \param The address of the remote udp echo serverechoClient.SetAttribute("MaxPackets",UintegerValue(1));echoClient.SetAttribute("Interval",TimeValue(Seconds(1.0)));echoClient.SetAttribute("PacketSize",UintegerValue(1024));// 设置客户端程序的工作属性ApplicationContainer clientApps = echoClient.Install(nodes.Get(0));clientApps.Start(Seconds(2.0));clientApps.Stop(Seconds(10.0));// 把客户端程序安装到0号节点,并设置其开始时间和结束时间。/*************************应用安装结束************************************/// 至此实现的功能为:0号节点作为客户端,再仿真开始后2s开始工作,// 向作为服务端的节点1每隔1s发送1个大小为1024的包// 服务器从9号端口收到后向客户端返回一个相同大小的包// 客户端与服务器在10s后均停止工作//有helper助手的帮助,无需关注套接字的细节问题/*************************仿真启动与结束************************************/AnimationInterface anim("p2p.xml");Simulator::Run();Simulator::Destroy();return 0;}

把文件保存在scratch目录下,执行指令

./ns3 run scratch/new_fist.cc

相信你会在终端看到两个节点有来有回的交际~

还是那句话,记住,明天是个大晴天~