上一节我们似乎只是简单地说了“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~要是连网络节点都无法定义,怎么能继续进行呢?
#include "ns3/internet-module.h": 定义了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,跳转到该函数的源码,我们会看到:
NetDeviceContainer
PointToPointHelper::Install(Ptr a, Ptr b)
{
NetDeviceContainer container;
......
......
return container;
}
是否清晰一些?该函数会返回一个网络设备容器类的变量,因此在脚本中使用devices去接收这个函数。如果还是不太理解为什么要单独拎出来一个devices变量的话,没有关系,我们先姑且这样想:目前节点有了,设备呢?助手是可以创建,可是他屏蔽掉了创建的细节,我们需要”看得见的设备“,因此NetDeviceContainer devices便被创建出来了,指不定后续要用它来干点什么呢~
后续的过程会让我们对这个问题更加清晰的,别急~
截至目前,我们的拓扑结构是这样的:
一般来说,NetDevice负责链路层协议、Channel负责物理层协议。至此,我们的拓扑可以完成链路层和物理层的通信。
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函数的逻辑:
目前位置整个网络的部署情况如下图所示
图中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));
应用安装结束,至此实现的功能为
最后别忘了开启和结束模拟:
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,指定节点数量2
PointToPointHelper 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 server
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号节点,并设置其开始时间和结束时间。
/*************************应用安装结束************************************/
// 至此实现的功能为: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
相信你会在终端看到两个节点有来有回的交际~
还是那句话,记住,明天是个大晴天~