“ 动起手来P2P “——《开源网络模拟器NS-3架构与实践(周之迪)》学习之旅(2)

随便说说

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

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

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

问题来了

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

“ 动起手来P2P “——《开源网络模拟器NS-3架构与实践(周之迪)》学习之旅(2)_第1张图片

 认识头文件

 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便被创建出来了,指不定后续要用它来干点什么呢~

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

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

“ 动起手来P2P “——《开源网络模拟器NS-3架构与实践(周之迪)》学习之旅(2)_第2张图片

 一般来说,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对象

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

“ 动起手来P2P “——《开源网络模拟器NS-3架构与实践(周之迪)》学习之旅(2)_第3张图片

图中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,指定节点数量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

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

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

你可能感兴趣的:(学习,计算机网络,网络,ubuntu,p2p)