【NS3】入门笔记(部分) 翻译自ns3-tutorial

NS3-Tutorial

Introduction

ns-3不是ns-2的向后兼容的扩展;它是一个新的模拟器。这两个模拟器都是用C++编写的,但ns-3是一个新的模拟器,不支持ns-2的API。

tutorial的用法

  • Try to download and build a copy;
  • Try to run a few sample programs;
  • Look at simulation output, and try to adjust it.

Getting Started

本节的目的是让用户从一个可能从未安装过ns-3的机器开始进入工作状态。

Conceptual Overview

解释系统中的一些核心概念和抽象。一些网络中常用的术语,但在ns-3中具有特定的含义。

Key Abstractions

Node

因为ns-3是一个网络模拟器,而不是专门的互联网模拟器,我们有意不使用主机(host)这个术语。相反,使用节点(node)

在ns-3中,基本的计算设备抽象被称为节点。这个抽象概念在C++中由Node类表示。Node类提供了管理模拟中计算设备的表示的方法。

你应该把**一个节点看作是一台计算机,你将向其添加功能。人们会添加一些东西,如应用程序、协议栈和外围卡及其相关的驱动程序,**以使计算机能够进行有用的工作。我们在ns-3中使用同样的基本模型。

Application

用户通常会运行一个应用程序,获取并使用由系统软件控制的资源来完成一些目标。

通常情况下,系统和应用软件之间的分界线是在操作系统陷阱中发生的权限级别变化上进行的。在ns-3中,没有操作系统的真正概念,特别是没有权限级别或系统调用的概念。然而,我们确实有一个应用程序的概念。

像软件应用程序在计算机上运行以执行 "真实世界 "中的任务一样,ns-3应用程序在ns-3节点上运行以驱动模拟世界中的仿真。

在ns-3中,产生一些要模拟的活动的用户程序的基本抽象是应用程序。这个抽象概念在C++中由Application类表示。应用程序类提供了管理我们的用户级应用程序在模拟中的表现的方法。开发人员要在面向对象的编程意义上对Application类进行专业化,以创建新的应用程序。

在本教程中,我们将使用名为UdpEchoClientApplication和UdpEchoServerApplication的Application类的特殊化。正如你所期望的那样,这些应用程序组成了一个客户端/服务器应用程序集,用于生成和呼应模拟的网络数据包

Channel

在ns-3的模拟世界中,人们将一个节点连接到一个代表通信通道的对象上。在这里,基本的通信子网络抽象被称为通道,在C++中用Channel类来表示。

Channel类提供了管理通信子网对象和将节点连接到它们的方法。

在本教程中,我们将使用名为CsmaChannel、PointToPointChannel和WifiChannel的专门版本的通道。例如,CsmaChannel是一个通信子网的模型,它实现了载波感应多路存取通信介质。这给了我们类似以太网的功能。

Net Device

如果没有控制硬件的软件驱动程序,网卡将无法工作。在Unix(或Linux)中,一个外围硬件被归类为一个设备。设备使用设备驱动程序进行控制,而网络设备(NIC)则使用网络设备驱动程序进行控制,统称为net设备。在Unix和Linux中,你用诸如eth0这样的名字来指代这些网络设备。

在ns-3中,网络设备的抽象涵盖了软件驱动和模拟的硬件。网络设备被 "安装 "在一个节点上,以使该节点能够通过通道与模拟中的其他节点通信。就像在真实的计算机中,一个节点可以通过多个网络设备连接到一个以上的通道。

网络设备的抽象在C++中由NetDevice类表示。NetDevice类提供了管理与Node和Channel对象的连接的方法

我们将在本教程中使用NetDevice的几个专门版本,称为CsmaNetDevice、PointToPointNetDevice和WifiNetDevice。就像以太网网卡被设计为与以太网网络一起工作一样,CsmaNetDevice被设计为与CsmaChannel一起工作;PointToPointNetDevice被设计为与PointToPointChannel一起工作,WifiNetNevice被设计为与WifiChannel一起工作。

Topology Helper 拓扑结构Helper

由于将NetDevices连接到Nodes,将NetDevices连接到Channels,分配IP地址等等,都是ns-3中常见的任务,我们提供了所谓的拓扑帮助器,使之尽可能的简单。例如,创建一个NetDevice,添加一个MAC地址,在节点上安装该网络设备,配置节点的协议栈,然后将NetDevice连接到通道上,可能需要许多不同的ns-3核心操作。甚至需要更多的操作来将多个设备连接到多点通道上,然后将各个网络连接到一起,形成国际网络。我们提供了拓扑帮助对象,将这些不同的操作结合到一个易于使用的模型中,以方便你的使用。

A First ns-3 Script

Boilerplate 规章制度

Module Includes

在构建过程中,每个ns-3的包含文件都被放在一个叫做ns3的目录下(在构建目录下),以帮助避免包含文件名称的冲突。ns3/core-module.h文件对应于你在下载的发行版中src/core目录下找到的ns-3模块。如果你列出这个目录,你会发现有大量的头文件。当你进行构建时,Waf会根据你的配置,将公共头文件放在适当的构建/调试或构建/优化目录下的ns3目录中。Waf还将自动生成一个模块包含文件来加载所有的公共头文件。

Ns3 Namespace

using namespace ns3;

ns-3项目是在一个名为ns3的C++命名空间中实现的。这将所有与 ns-3 有关的声明归入全局命名空间之外的范围

Logging

NS_LOG_COMPONENT_DEFINE ("FirstScriptExample");

NS_LOG_COMPONENT_DEFINE文档。我不会在这里重复文档,但总结一下,这一行声明了一个名为FirstScriptExample的日志组件,允许你通过引用名称来启用和禁用控制台消息记录。

Main Function

Time::SetResolution (Time::NS);

将时间分辨率设置为1纳秒,这刚好是默认值。

分辨率是可以表示的最小的时间值(也是两个时间值之间可以表示的最小的差异)

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

启用Echo客户端和Echo服务器应用程序中内置的两个日志组件。

LogComponentEnable(“UdpEchoClientApplication”, LOG_LEVEL_INFO)。
LogComponentEnable(“UdpEchoServerApplication”, LOG_LEVEL_INFO)。
如果你已经阅读了日志组件的文档,你会发现在每个组件上都有若干级别的日志粗略程度/细节,你可以启用。这两行代码在INFO级别为echo客户端和服务器启用调试日志。这将导致应用程序在模拟过程中发送和接收数据包时打印出信息。

he ns-3 logging subsystem is discussed in the Using the Logging Module section, so we’ll get to it later in this tutorial, but you can find out about the above statement by looking at the Core module, then expanding the Debugging tools book, and then selecting the Logging page. Click on Logging.

Topology Helpers

Now we will get directly to the business of creating a topology and running a simulation. We use the topology helper objects to make this job as easy as possible.

NodeContainer
NodeContainer nodes;
nodes.Create (2);

NodeContainer拓扑帮助器提供了一种方便的方式来创建、管理和访问我们为运行模拟而创建的任何Node对象。上面的第一行只是声明了一个NodeContainer,我们称之为nodes。第二行调用nodes对象的Create方法,要求容器创建两个节点。正如Doxygen中所描述的那样,容器向下调用ns-3系统本身来创建两个Node对象,并在内部存储指向这些对象的指针。

PointToPointHelper

脚本中的节点什么都不做。构建拓扑结构的下一步是将我们的节点连接成一个网络。我们支持的最简单的网络形式是两个节点之间的单一点对点链接。我们将在这里构建一个这样的链接。

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

在栈上实例化了一个PointToPointHelper对象。从高层的角度来看

告诉PointToPointHelper对象在创建PointToPointNetDevice对象时使用 “5Mbps”(每秒5兆比特)作为 "DataRate "的值。
告诉PointToPointHelper使用值 “2ms”(两毫秒)作为它随后创建的每个点对点通道的传播延迟值。

NetDeviceContainer

我们有一个NodeContainer,它包含两个节点。我们有一个PointToPointHelper,它已经准备好制造PointToPointNetDevices并在它们之间连接PointToPointChannel对象。

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

需要有一个所有被创建的NetDevice对象的列表,所以我们使用NetDeviceContainer来保存它们

第一行声明了上面提到的设备容器,

第二行做了繁重的工作。PointToPointHelper的Install方法需要一个NodeContainer作为参数。在内部,一个NetDeviceContainer被创建。对于NodeContainer中的每个节点(点对点链接必须正好有两个),一个PointToPointNetDevice被创建并保存在设备容器中。一个PointToPointChannel被创建,两个PointToPointNetDevice被连接。当对象被PointToPoint帮助器创建时,先前在帮助器中设置的属性被用来初始化创建对象中的相应属性。

在执行pointToPoint.Install (nodes)调用后,

  • 我们将有两个节点,
  • 每个节点都有一个已安装的点对点网络设备,
  • 它们之间有一个单一的点对点通道。这两个设备将被配置为在有两毫秒传输延迟的信道上以每秒5兆比特的速度传输数据。
InternetStackHelper

协议栈

InternetStackHelper stack;
stack.Install (nodes);

当它被执行时,它将在节点容器中的每个节点上安装一个互联网栈(TCP、UDP、IP等)。

Ipv4AddressHelper
Ipv4AddressHelper address;
address.SetBase ("10.1.1.0", "255.255.255.0");

将我们节点上的设备与IP地址联系起来。我们提供一个拓扑帮助器来管理IP地址的分配。唯一用户可见的API是设置基础IP地址和网络掩码,以便在执行实际的地址分配时使用

告诉它应该从网络10.1.1.0开始分配IP地址,使用掩码255.255.255.0来定义可分配的位。默认情况下,分配的地址将从1开始并单调地增加,所以从这个基数分配的第一个地址将是10.1.1.1,接着是10.1.1.2,等等。

低级别的ns-3系统实际上记住了所有分配的IP地址,如果你不小心导致同一个地址产生两次,就会产生一个致命的错误(顺便说一下,这是一个非常难以调试的错误)。

Ipv4InterfaceContainer interfaces = address.Assign (devices);

使用Ipv4Interface对象在IP地址和设备之间建立了联系。就像我们有时需要一个由助手创建的网络设备列表供将来参考一样,我们有时需要一个Ipv4Interface对象的列表。Ipv4InterfaceContainer提供了这个功能。

Applications

现在我们已经建立了一个点对点的网络,并安装了堆栈和分配了IP地址。在这一点上,我们需要的是应用程序来产生流量。

UdpEchoServerHelper
UdpEchoServerHelper echoServer (9);

ApplicationContainer serverApps = echoServer.Install (nodes.Get (1));
serverApps.Start (Seconds (1.0));
serverApps.Stop (Seconds (10.0));

在我们先前创建的一个节点上设置一个UDP echo server application

UdpEchoServerHelper echoServer (9)我们要求将端口号作为构造函数的参数

我们现在看到echoServer.Install将在我们用来管理节点的NodeContainer的索引号上找到的节点上安装一个UdpEchoServerApplication。Install将返回一个容器,该容器持有由帮助器创建的所有应用程序的指针(在这种情况下是一个,因为我们传递了一个包含一个节点的NodeContainer)。

应用程序需要一个时间来 "开始 "产生流量,并可能需要一个可选的时间来 “停止”。我们提供这两个时间。这些时间是通过ApplicationContainer的Start和Stop方法设置的。这些方法需要时间参数。

回声服务器应用程序在模拟的一秒钟内开始(启用自己),在模拟的十秒钟内停止(禁用自己)。

UdpEchoClientHelper
UdpEchoClientHelper echoClient (interfaces.GetAddress (1), 9);
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));

对于回声客户端,我们需要设置五个不同的Attributes。

回顾一下,我们使用了一个Ipv4InterfaceContainer来跟踪我们分配给设备的IP地址

  • 接口容器中的第一个接口对应于节点容器中第一个节点的IP地址。
  • 接口容器中的第2个接口将对应于节点容器中第2个节点的IP地址。

因此,在第一行代码中(从上面开始),我们正在创建帮助器,并告诉它将客户端的远程地址设置为分配给服务器所在节点的IP地址。我们还告诉它安排向第9端口发送数据包。

MaxPackets "属性告诉客户端在模拟过程中我们允许它发送的最大数据包数量

Interval"属性告诉客户机在数据包之间要等待多长时间,而

"PacketSize "属性告诉客户机数据包的大小

就像在回声服务器的情况下,我们告诉回声客户端开始和停止,但在这里,我们在服务器启用一秒钟后启动客户端(在模拟的两秒钟内)。

Simulator

Simulator::Run ();

在这一点上,我们需要做的是实际运行模拟。这是用全局函数Simulator::Run完成的。

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

我们实际上在模拟器中安排了1.0秒、2.0秒的事件和10.0秒的两个事件。当Simulator::Run被调用时,系统将开始查看预定事件列表并执行它们。

  • 首先,它将运行1.0秒的事件,这将启用回声服务器应用程序(这个事件可能反过来安排许多其他事件)。然后它将运行计划在t=2.0秒的事件,这将启动回声客户端应用程序。同样,这个事件可能会安排许多其他事件。在回声客户端应用程序中的启动事件实现将通过向服务器发送一个数据包来开始模拟的数据传输阶段。

  • 向服务器发送数据包的行为将触发一连串的事件,这些事件将在幕后自动安排,并根据我们在脚本中设置的各种时间参数来执行数据包回声的机械操作。

  • 最终,由于我们只发送了一个数据包(记得MaxPackets属性被设置为一个),由那个单一的客户端回声请求引发的事件链将逐渐减少,模拟将进入空闲状态。一旦发生这种情况,剩下的事件将是服务器和客户端的停止事件。

当这些事件被执行后,没有进一步的事件需要处理,Simulator::Run就会返回。仿真就完成了。

  Simulator::Destroy ();
  return 0;
}

剩下的就是清理了。调用Simulator::Destroy并退出。

When the simulator will stop?

ns-3是一个**离散事件(**DE)模拟器。在这样的模拟器中,每个事件都与它的执行时间相关联,模拟通过执行事件在模拟时间的时间顺序进行。

+  Simulator::Stop (Seconds (11.0));
   Simulator::Run ();
   Simulator::Destroy ();
   return 0;
 }

在调用Simulator::Run之前调用Simulator::Stop是很重要的;否则,Simulator::Run可能永远不会将控制权返回给主程序来执行Stop!

Building Your Script

output:

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.418s)
Sent 1024 bytes to 10.1.1.2
Received 1024 bytes from 10.1.1.1
Received 1024 bytes from 10.1.1.2

你看到回声客户端上的日志组件显示它已经向10.1.1.2的回声服务器发送了一个1024字节的数据包。

你也看到回声服务器上的日志组件说它已经收到了来自10.1.1.1的1024字节。

**回声服务器默默地回声了该数据包,**你看到回声客户端记录说它已经从服务器收到了它的数据包。

Tweaking

Using the Logging Module

我们提供了一个可选择的、多级别的消息记录方法。日志可以被完全禁用,也可以逐个组件地启用,或者全局地启用;它还提供了可选择的粗体等级。

Logging Overview

  • LOG_ERROR — Log error messages (associated macro: NS_LOG_ERROR);
  • LOG_WARN — Log warning messages (associated macro: NS_LOG_WARN);
  • LOG_DEBUG — Log relatively rare, ad-hoc debugging messages (associated macro: NS_LOG_DEBUG);
  • LOG_INFO — Log informational messages about program progress (associated macro: NS_LOG_INFO);
  • LOG_FUNCTION — 记录描述每个函数调用的信息 (two associated macros: NS_LOG_FUNCTION, used for 成员函数, and NS_LOG_FUNCTION_NOARGS, used for 静态函数);
  • LOG_LOGIC – 记录函数内逻辑流程的信息 (associated macro: NS_LOG_LOGIC);
  • LOG_ALL —记录描述每个函数调用的信息(no associated macro).

每个级别都可以单独或累计请求;并且可以使用shell环境变量(NS_LOG)或通过日志系统函数调用来设置日志。

Enabling Logging

从这里开始,我将假设你使用的是一个使用 "VARIABLE=value "语法的sh-like shell。如果你使用的是类似于csh的shell,那么你将不得不把我的例子转换成这些shell所要求的 "setenv VARIABLE value "的语法。

$ Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.413s)
Sent 1024 bytes to 10.1.1.2
Received 1024 bytes from 10.1.1.1
Received 1024 bytes from 10.1.1.2

事实证明,你在上面看到的 "发送 "和 "接收 "信息实际上是UdpEchoClientApplication和UdpEchoServerApplication的日志信息。例如,我们可以通过NS_LOG环境变量设置其日志级别,要求客户端应用程序打印更多信息。

UDP 回声客户端应用程序对 scratch/myfirst.cc 中的以下一行代码做出了响应。

LogComponentEnable("UdpEchoClientApplication", LOG_LEVEL_INFO);

这行代码启用了LOG_LEVEL_INFO级别的日志记录。

  • 当我们传递一个日志级别标志时,我们实际上是启用了给定的级别和所有更低的级别。在这个例子中,我们启用了NS_LOG_INFO、NS_LOG_DEBUG、NS_LOG_WARN和NS_LOG_ERROR。

我们可以通过这样设置NS_LOG环境变量来提高日志级别,获得更多的信息,而不必改变脚本和重新编译。

$ export NS_LOG=UdpEchoClientApplication=level_all

把shell环境变量NS_LOG设置为字符串。

This sets the shell environment variable NS_LOG to the string,

UdpEchoClientApplication=level_all

如果你以这种方式设置NS_LOG来运行脚本,ns-3日志系统会接收到这个变化,你应该看到以下输出。

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.404s)
UdpEchoClientApplication:UdpEchoClient()
UdpEchoClientApplication:SetDataSize(1024)
UdpEchoClientApplication:StartApplication()
UdpEchoClientApplication:ScheduleTransmit()
UdpEchoClientApplication:Send()
Sent 1024 bytes to 10.1.1.2
Received 1024 bytes from 10.1.1.1
UdpEchoClientApplication:HandleRead(0x6241e0, 0x624a20)
Received 1024 bytes from 10.1.1.2
UdpEchoClientApplication:StopApplication()
UdpEchoClientApplication:DoDispose()
UdpEchoClientApplication:~UdpEchoClient()
$ export 'NS_LOG=UdpEchoClientApplication=level_all|prefix_func'

注意,引号是必须的,因为我们用来表示OR操作的竖条也是Unix的管道连接器。

现在,如果你运行该脚本,你会看到日志系统确保来自给定日志组件的每条消息都以组件名称为前缀。

消息 "收到来自10.1.1.2的1024字节 "现在被清楚地识别为来自回声客户程序。

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.417s)
UdpEchoClientApplication:UdpEchoClient()
UdpEchoClientApplication:SetDataSize(1024)
UdpEchoClientApplication:StartApplication()
UdpEchoClientApplication:ScheduleTransmit()
UdpEchoClientApplication:Send()
UdpEchoClientApplication:Send(): Sent 1024 bytes to 10.1.1.2
Received 1024 bytes from 10.1.1.1
UdpEchoClientApplication:HandleRead(0x6241e0, 0x624a20)
UdpEchoClientApplication:HandleRead(): Received 1024 bytes from 10.1.1.2
UdpEchoClientApplication:StopApplication()
UdpEchoClientApplication:DoDispose()
UdpEchoClientApplication:~UdpEchoClient()
$ export 'NS_LOG=UdpEchoClientApplication=level_all|prefix_func:
               UdpEchoServerApplication=level_all|prefix_func'

现在,如果你运行这个脚本,你会看到来自echo客户端和服务器应用程序的所有日志信息。你可能会发现,这在调试问题时非常有用。

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.406s)
UdpEchoServerApplication:UdpEchoServer()
UdpEchoClientApplication:UdpEchoClient()
UdpEchoClientApplication:SetDataSize(1024)
UdpEchoServerApplication:StartApplication()
UdpEchoClientApplication:StartApplication()
UdpEchoClientApplication:ScheduleTransmit()
UdpEchoClientApplication:Send()
UdpEchoClientApplication:Send(): Sent 1024 bytes to 10.1.1.2
UdpEchoServerApplication:HandleRead(): Received 1024 bytes from 10.1.1.1
UdpEchoServerApplication:HandleRead(): Echoing packet
UdpEchoClientApplication:HandleRead(0x624920, 0x625160)
UdpEchoClientApplication:HandleRead(): Received 1024 bytes from 10.1.1.2
UdpEchoServerApplication:StopApplication()
UdpEchoClientApplication:StopApplication()
UdpEchoClientApplication:DoDispose()
UdpEchoServerApplication:DoDispose()
UdpEchoClientApplication:~UdpEchoClient()
UdpEchoServerApplication:~UdpEchoServer()

有时能够看到生成日志信息的模拟时间也很有用。你可以通过在prefix_time位上的OR来做到这一点。

$ export 'NS_LOG=UdpEchoClientApplication=level_all|prefix_func|prefix_time:
               UdpEchoServerApplication=level_all|prefix_func|prefix_time'
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.418s)
0s UdpEchoServerApplication:UdpEchoServer()
0s UdpEchoClientApplication:UdpEchoClient()
0s UdpEchoClientApplication:SetDataSize(1024)
1s UdpEchoServerApplication:StartApplication()
2s UdpEchoClientApplication:StartApplication()
2s UdpEchoClientApplication:ScheduleTransmit()
2s UdpEchoClientApplication:Send()
2s UdpEchoClientApplication:Send(): Sent 1024 bytes to 10.1.1.2
2.00369s UdpEchoServerApplication:HandleRead(): Received 1024 bytes from 10.1.1.1
2.00369s UdpEchoServerApplication:HandleRead(): Echoing packet
2.00737s UdpEchoClientApplication:HandleRead(0x624290, 0x624ad0)
2.00737s UdpEchoClientApplication:HandleRead(): Received 1024 bytes from 10.1.1.2
10s UdpEchoServerApplication:StopApplication()
10s UdpEchoClientApplication:StopApplication()
UdpEchoClientApplication:DoDispose()
UdpEchoServerApplication:DoDispose()
UdpEchoClientApplication:~UdpEchoClient()
UdpEchoServerApplication:~UdpEchoServer()

你可以看到,UdpEchoServer的构造函数是在0秒的模拟时间被调用的。这实际上是在模拟开始前发生的,但时间显示为0秒。

$ export 'NS_LOG=*=level_all|prefix_func|prefix_time'

上面的* 是记录组件的通配符。这将开启模拟中使用的所有组件中的所有日志记录。

重定向的使用

$ ./waf --run scratch/myfirst > log.out 2>&1

极其粗略的日志记录版本。我可以很容易地跟踪代码的进展,而不需要在调试器中设置断点和步入代码。我可以在我最喜欢的编辑器中编辑输出,搜索我所期望的东西,并看到我所不期望的事情发生。当我对出错的地方有一个大致的概念时,我就会过渡到调试器中,对问题进行细微的检查。

Adding Logging to your Code

示例

NS_LOG_INFO ("Creating Topology");

你将看不到你的新消息,因为它相关的日志组件(FirstScriptExample)还没有被启用。为了看到你的消息,你必须启用FirstScriptExample日志组件,其级别大于或等于NS_LOG_INFO

需要启用它

$ export NS_LOG=FirstScriptExample=info

Using Command Line Arguments

Overriding Default Attributes 覆盖默认属性

通过命令行参数来改变ns-3脚本

首先代码中写入

int
main (int argc, char *argv[])
{
  ...

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

  ...
}
$ ./waf --run "scratch/myfirst --PrintHelp"

这将要求 Waf 运行 scratch/myfirst 脚本,并将命令行参数 --PrintHelp 传给脚本。引号是用来区分哪个程序得到哪个参数的。

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.413s)
TcpL4Protocol:TcpStateMachine()
CommandLine:HandleArgument(): Handle arg name=PrintHelp value=
--PrintHelp: Print this help message.
--PrintGroups: Print the list of groups.
--PrintTypeIds: Print all TypeIds.
--PrintGroup=[group]: Print all TypeIds of group.
--PrintAttributes=[typeid]: Print all attributes of typeid.
--PrintGlobals: Print the list of globals.
$ ./waf --run "scratch/myfirst --PrintAttributes=ns3::PointToPointNetDevice"

系统将打印出这种网络设备的所有属性。在这些属性中,你将看到列出的是。

--ns3::PointToPointNetDevice::DataRate=[32768bps]:
  The default data rate for point to point links

Hooking Your Own Values

Using the Tracing System

仿真的全部意义在于产生输出,以便进一步研究,而ns-3追踪系统是这方面的一个主要机制。

由于ns-3是一个C++程序,可以使用从C++程序生成输出的标准设施。

#include <iostream>
...
int main ()
{
  ...
  std::cout << "The value of x is " << x << std::endl;
  ...
}

ns-3追踪系统的基本目标是。

  1. 对于基本的任务,追踪系统应该允许用户为流行的追踪源产生标准的追踪,并且可以自定义哪些对象产生追踪。
  2. 中级用户必须能够扩展追踪系统,修改生成的输出格式,或者插入新的追踪源,而不需要修改模拟器的核心。
  3. 高级用户可以修改模拟器的核心,增加新的跟踪源和汇。

ASCII Tracing

ns-3提供了帮助功能,包裹了低级别的跟踪系统,以帮助你在配置一些容易理解的数据包跟踪时涉及的细节。如果你启用了这个功能,你会看到ASCII文件的输出–因此而得名。对于那些熟悉ns-2输出的人来说,这种类型的跟踪类似于许多脚本所产生的out.tr。

在我们的 scratch/myfirst.cc 脚本中添加一些 ASCII 追踪输出。在调用 Simulator::Run () 之前,添加以下几行代码。

AsciiTraceHelper ascii;
pointToPoint.EnableAsciiAll (ascii.CreateFileStream ("myfirst.tr"));

这段代码使用了一个帮助对象来帮助创建ASCII码的跟踪。第二行包含两个嵌套的方法调用。内部 "方法,CreateFileStream()使用了一个未命名的对象习惯法,在堆栈中创建一个文件流对象(没有对象名),并将其传递给被调用的方法。

myfirst.tr的文件。由于Waf的工作方式,这个文件不是在本地目录下创建的,它默认是在版本库的顶级目录下创建的。如果你想控制痕迹的保存位置,你可以使用Waf的-cwd选项来指定。

Parsing Ascii Traces
  • +: 设备队列上发生了一个enqueue操作。
  • -: 在设备队列上发生了一个去queue操作。
  • d: 一个数据包被丢弃,通常是因为队列已满。
  • r: 网络设备收到了一个数据包。

举例

+
2
/NodeList/0/DeviceList/0/$ns3::PointToPointNetDevice/TxQueue/Enqueue
ns3::PppHeader (
  Point-to-Point Protocol: IP (0x0021))
  ns3::Ipv4Header (
    tos 0x0 ttl 64 id 0 protocol 17 offset 0 flags [none]
    length: 1052 10.1.1.1 > 10.1.1.2)
    ns3::UdpHeader (
      length: 1032 49153 > 9)
      Payload (size=1024)

我们有一个 "+"字符,所以这相当于在发送队列上的一个enqueue操作。第二部分(参考号1)是以秒为单位的模拟时间。你可能还记得,我们要求UdpEchoClientApplication在两秒内开始发送数据包。在这里我们看到了确认,这确实是在发生。

可以把追踪命名空间看作是文件系统命名空间的一部分。该**命名空间的根是NodeList。**它对应于ns-3核心代码中管理的一个容器,它包含了所有在脚本中创建的节点。就像文件系统在根目录下可能有目录一样,我们在NodeList中可能有节点编号。因此,**字符串/NodeList/0指的是NodeList中的第2个节点,**我们通常认为是 “节点0”。在每个节点中,都有一个已经安装的设备列表。这个列表接下来出现在命名空间中。你可以看到这个追踪事件来自DeviceList/0,它是安装在节点中的第2个设备。

$ns3::PointToPointNetDevice告诉你什么样的设备是在节点0的设备列表的第2个位置。回顾一下,在参考00处发现的操作+意味着设备的发送队列上发生了一个enqueue操作。这反映在 "跟踪路径 "的最后一段,即TxQueue/Enqueue。

追踪文件中的第三行显示数据包被有回声服务器的节点上的网络设备接收。

r
2.25732
/NodeList/1/DeviceList/0/$ns3::PointToPointNetDevice/MacRx
  ns3::Ipv4Header (
    tos 0x0 ttl 64 id 0 protocol 17 offset 0 flags [none]
    length: 1052 10.1.1.1 > 10.1.1.2)
    ns3::UdpHeader (
      length: 1032 49153 > 9)
      Payload (size=1024)

现在跟踪操作是r,模拟时间增加到2.25732秒。如果你一直紧跟教程的步骤,这意味着你已经把网络设备的DataRate和通道Delay设置为它们的默认值。这个时间应该很熟悉,因为你以前在前面的章节中见过它。

跟踪源命名空间条目(参考02)已经改变,以反映该事件来自节点1(/NodeList/1)和包接收跟踪源(/MacRx)。通过查看文件中其他的跟踪记录,你应该很容易跟踪数据包在拓扑结构中的进展。

PCAP Tracing

ns-3设备助手也可以用来创建.pcap格式的跟踪文件。首字母缩写 pcap(通常用小写)代表数据包捕获,实际上是一个包括.pcap文件格式定义的API。

能够读取和显示这种格式的最流行的程序是Wireshark

用于启用 pcap 追踪的代码是一个单行代码。

pointToPoint.EnablePcapAll ("myfirst");

注意,我们只传递了字符串 “myfirst”,而不是 "myfirst.cap “或类似的东西。这是因为这个参数是一个前缀,而不是一个完整的文件名。助手实际上将为模拟中的每个点对点设备创建一个跟踪文件。文件名将使用前缀、节点号、设备号和”.pcap "后缀来建立。

Reading output with tcpdump

使用 tcpdump 来查看 pcap 文件。

$ tcpdump -nn -tt -r myfirst-0-0.pcap
reading from file myfirst-0-0.pcap, link-type PPP (PPP)
2.000000 IP 10.1.1.1.49153 > 10.1.1.2.9: UDP, length 1024
2.514648 IP 10.1.1.2.9 > 10.1.1.1.49153: UDP, length 1024

tcpdump -nn -tt -r myfirst-1-0.pcap
reading from file myfirst-1-0.pcap, link-type PPP (PPP)
2.257324 IP 10.1.1.1.49153 > 10.1.1.2.9: UDP, length 1024
2.257324 IP 10.1.1.2.9 > 10.1.1.1.49153: UDP, length 1024

你可以在myfirst-0-0.cap(客户端设备)的转储中看到,回波数据包是在模拟过程中的2秒发送的。如果你看一下第二个转储(myfirst-1-0.cap),你可以看到该数据包在2.257324秒被接收。在第二个转储中,你看到数据包在2.257324秒时被回传,最后,你看到数据包在2.514648秒时被客户端接收。

Reading output with Wireshark

Building Topologies

Building a Bus Network Topology

s-3提供了一个网络设备和通道,我们称之为CSMA(载波感应多路存取)。

ns-3 CSMA设备以以太网的精神模拟了一个简单的网络。一个真正的以太网使用CSMA/CD(带碰撞检测的载波感应多路存取)方案,用指数级增加的后退来争夺共享的传输介质。ns-3的CSMA设备和通道模型只是其中的一个子集。

相较于first,cc,有些不同的地方

bool verbose = true;
uint32_t nCsma = 3;

CommandLine cmd;
cmd.AddValue ("nCsma", "Number of \"extra\" CSMA nodes/devices", nCsma);
cmd.AddValue ("verbose", "Tell echo applications to log if true", verbose);

cmd.Parse (argc, argv);

if (verbose)
  {
    LogComponentEnable("UdpEchoClientApplication", LOG_LEVEL_INFO);
    LogComponentEnable("UdpEchoServerApplication", LOG_LEVEL_INFO);
  }

nCsma = nCsma == 0 ? 1 : nCsma;

verbose标志来确定UdpEchoClientApplication和UdpEchoServerApplication的日志组件是否被启用。这个标志默认为 “true”(日志组件被启用

CommandLine cmd允许你通过命令行参数改变CSMA网络上的设备数量。当我们允许在命令行参数部分改变发送数据包的数量时

nCsma = nCsma == 0 ? 1 : nCsma; 确保你至少有一个 "额外 "的节点。

后面和first.cc很像,略

Ipv4GlobalRoutingHelper::PopulateRoutingTables ();

每个节点的行为就像它是一个OSPF路由器,在幕后与所有其他路由器进行即时和神奇的通信。每个节点生成链接广告,并将其直接传达给全局路由管理器,后者使用这些全局信息为每个节点构建路由表。

接下来我们启用 pcap 追踪。在点对点帮助器中启用 pcap 追踪的第一行代码现在你应该很熟悉了。

第二行是在CSMA帮助器中启用pcap跟踪,有一个额外的参数你还没有遇到过。

pointToPoint.EnablePcapAll ("second");
csma.EnablePcap ("second", csmaDevices.Get (1), true);

在这个例子中,我们将选择CSMA网络中的一个设备,要求它对网络进行混杂嗅探,从而模拟tcpdump的工作。

如果你是在一台Linux机器上,你可能会做一些类似tcpdump -i eth0的事情来获得跟踪。在这种情况下,我们用csmaDevices.Get(1)指定设备,它选择容器中的第一个设备。将最后一个参数设置为 “true”,就可以进行混杂捕获。

$ cp examples/tutorial/second.cc scratch/mysecond.cc
$ ./waf

我们使用second.cc文件作为我们的回归测试之一,以验证它是否完全按照我们认为的那样工作,以使你的教程体验是积极的。这意味着项目中已经存在一个名为second的可执行文件。

$ export NS_LOG=
$ ./waf --run scratch/mysecond
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.415s)
Sent 1024 bytes to 10.1.2.4
Received 1024 bytes from 10.1.1.1
Received 1024 bytes from 10.1.2.4

第一条信息 “发送1024字节到10.1.2.4”,是UDP回声客户端向服务器发送一个数据包。在这种情况下,服务器在另一个网络上(10.1.2.0)。

第二条消息,"收到来自10.1.1.1的1024字节,"是来自UDP回波服务器,当它收到回波数据包时产生。

最后一条消息,“收到来自10.1.2.4的1024字节”,是来自回声客户端,表明它已经收到了来自服务器的回声。

如果你现在去看顶层目录,你会发现三个跟踪文件。

second-0-0.pcap  second-1-0.pcap  second-2-0.pcap

这些文件的命名。它们都有相同的形式:**<名称>-<节点>-<设备>.pcap。**例如,列表中的第一个文件是 second-0-0.cap,这是来自零号节点、零号设备的 pcap 跟踪。

// Default Network Topology
//
//       10.1.1.0
// n0 -------------- n1   n2   n3   n4
//    point-to-point  |    |    |    |
//                    ================
//                      LAN 10.1.2.0

如果你参考本节开始时的拓扑图,你会发现零号节点是点对点链路的最左边的节点,

节点一是同时拥有点对点设备和CSMA设备的节点。

节点二是CSMA网络上的第一个 "额外 "节点,它的设备零被选为捕获混杂模式跟踪的设备。

$ tcpdump -nn -tt -r second-0-0.pcap
reading from file second-0-0.pcap, link-type PPP (PPP)
2.000000 IP 10.1.1.1.49153 > 10.1.2.4.9: UDP, length 1024
2.017607 IP 10.1.2.4.9 > 10.1.1.1.49153: UDP, length 1024

转储的第一行表明链接类型是PPP(点对点),

回声包通过与IP地址10.1.1.1相关的设备离开零号节点,前往IP地址10.1.2.4(最右边的CSMA节点)。

这个数据包将在点对点链路上移动,被节点一上的点对点网络设备接收。

$ tcpdump -nn -tt -r second-1-0.pcap
reading from file second-1-0.pcap, link-type PPP (PPP)
2.003686 IP 10.1.1.1.49153 > 10.1.2.4.9: UDP, length 1024
2.013921 IP 10.1.2.4.9 > 10.1.1.1.49153: UDP, length 1024

来自IP地址10.1.1.1的数据包(在2.000000秒时发送)向IP地址10.1.2.4的方向出现在这个接口上。现在,在这个节点的内部,数据包将被转发到CSMA接口,我们应该看到它在该设备上弹出,前往其最终目的地。

$ tcpdump -nn -tt -r second-2-0.pcap
reading from file second-2-0.pcap, link-type EN10MB (Ethernet)
2.007698 ARP, Request who-has 10.1.2.4 (ff:ff:ff:ff:ff:ff) tell 10.1.2.1, length 50
2.007710 ARP, Reply 10.1.2.4 is-at 00:00:00:00:00:06, length 50
2.007803 IP 10.1.1.1.49153 > 10.1.2.4.9: UDP, length 1024
2.013815 ARP, Request who-has 10.1.2.1 (ff:ff:ff:ff:ff:ff) tell 10.1.2.4, length 50
2.013828 ARP, Reply 10.1.2.1 is-at 00:00:00:00:00:03, length 50
2.013921 IP 10.1.2.4.9 > 10.1.1.1.49153: UDP, length 1024

正如你所看到的,现在的链接类型是 “Ethernet”。不过,出现了一些新东西。

总线网络需要ARP,即地址解析协议。节点一知道它需要向IP地址10.1.2.4发送数据包,但它不知道相应节点的MAC地址。它在CSMA网络上进行广播(ff:ff:ff:ff:ff:ff:ff:ff),询问拥有IP地址10.1.2.4的设备。在这种情况下,最右边的节点回复说它在MAC地址00:00:00:00:00:06。

注意,节点二没有直接参与这个交换,而是在嗅探网络并报告它所看到的所有流量。

这种交换在下面几行中可以看到。

2.007698 ARP, Request who-has 10.1.2.4 (ff:ff:ff:ff:ff:ff) tell 10.1.2.1, length 50
2.007710 ARP, Reply 10.1.2.4 is-at 00:00:00:00:00:06, length 50

然后节点一,设备一继续前进,向IP地址为10.1.2.4的UDP回音服务器发送回音包。

2.007803 IP 10.1.1.1.49153 > 10.1.2.4.9: UDP, length 1024

服务器收到回声请求,并把数据包转过来,试图把它发回给源。服务器知道这个地址是在另一个网络上,它通过IP地址10.1.2.1到达。这是因为我们初始化了全局路由,它已经为我们想好了这一切。但是,回声服务器节点不知道第一个CSMA节点的MAC地址,所以它必须像第一个CSMA节点那样进行ARP查找。

2.013815 ARP, Request who-has 10.1.2.1 (ff:ff:ff:ff:ff:ff) tell 10.1.2.4, length 50
2.013828 ARP, Reply 10.1.2.1 is-at 00:00:00:00:00:03, length 50

然后,服务器将回波发送至转发节点。

2.013921 IP 10.1.2.4.9 > 10.1.1.1.49153: UDP, length 1024

回看点对点链接的最右边的节点。

$ tcpdump -nn -tt -r second-1-0.pcap

现在你可以看到回波的数据包作为跟踪转储的最后一行回到了点对点链路上。

reading from file second-1-0.pcap, link-type PPP (PPP)
2.003686 IP 10.1.1.1.49153 > 10.1.2.4.9: UDP, length 1024
2.013921 IP 10.1.2.4.9 > 10.1.1.1.49153: UDP, length 1024

看一下产生回声的节点

$ tcpdump -nn -tt -r second-0-0.pcap

该回波数据包在2.017607秒时到达源头。

reading from file second-0-0.pcap, link-type PPP (PPP)
2.000000 IP 10.1.1.1.49153 > 10.1.2.4.9: UDP, length 1024
2.017607 IP 10.1.2.4.9 > 10.1.1.1.49153: UDP, length 1024

利用命令行把节点数目改为4个

$ ./waf --run "scratch/mysecond --nCsma=4"
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.405s)
At time 2s client sent 1024 bytes to 10.1.2.5 port 9
At time 2.0118s server received 1024 bytes from 10.1.1.1 port 49153
At time 2.0118s server sent 1024 bytes to 10.1.1.1 port 49153
At time 2.02461s client received 1024 bytes from 10.1.2.5 port 9

现在回声服务器已经被重新定位到CSMA节点中的最后一个,也就是10.1.2.5

你有可能不满意由CSMA网络中的旁观者产生的跟踪文件。你可能真的想得到一个单一设备的跟踪,你可能对网络上的任何其他流量不感兴趣。

pointToPoint.EnablePcap ("second", p2pNodes.Get (0)->GetId (), 0);
csma.EnablePcap ("second", csmaNodes.Get (nCsma)->GetId (), 0, false);
csma.EnablePcap ("second", csmaNodes.Get (nCsma-1)->GetId (), 0, false);

认识到NodeContainers包含指向ns-3 Node对象的指针。节点对象有一个叫做GetId的方法,它将返回该节点的ID,也就是我们要找的节点编号。

在复杂的拓扑结构中,使用GetId方法可以更容易地确定节点编号。

旧的跟踪文件从顶层目录中清除掉

$ rm *.pcap
$ rm *.tr

重新跑程序,得

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.407s)
At time 2s client sent 1024 bytes to 10.1.2.101 port 9
At time 2.0068s server received 1024 bytes from 10.1.1.1 port 49153
At time 2.0068s server sent 1024 bytes to 10.1.1.1 port 49153
At time 2.01761s client received 1024 bytes from 10.1.2.101 port 9

现在回声服务器位于10.1.2.101,这相当于有100个 "额外的 "CSMA节点,回声服务器位于最后一个节点。

second-0-0.pcap  second-100-0.pcap  second-101-0.pcap

文件second-0-0.cap是 "最左边 "的点对点设备,它是回声数据包的来源。文件second-101-0.cap对应的是最右边的CSMA设备,也就是回音服务器所在的位置。

在回声服务器节点上启用 pcap 跟踪的调用的最后一个参数是 false。这意味着在该节点上收集的跟踪是以非混杂模式进行的。

混杂和非混杂跟踪之间的区别

Models, Attributes and Reality

无论何时,当人们使用模拟时,重要的是要明白到底什么是被建模的,什么不是。例如,我们很容易把上一节中使用的CSMA设备和信道想成是真实的以太网设备;并期望仿真结果能直接反映真实的以太网中会发生什么。事实并非如此。

根据定义,模型是对现实的抽象。仿真脚本作者最终有责任确定整个仿真的所谓 "准确范围 "和 “适用领域”,因此也包括其组成部分。

在某些情况下,比如Csma,要确定哪些东西没有被建模是相当容易的。通过阅读模型描述(csma.h),你可以发现CSMA模型中没有碰撞检测,并决定在你的仿真中如何使用它,或者你可能想在结果中加入什么注意事项。在其他情况下,可以很容易地配置一些行为,这些行为可能与你可以出去买的任何现实不一致。事实证明,花一些时间来调查一些这样的例子是值得的,你可以很容易地在你的模拟中转向现实的界限之外。

正如你所看到的,NS-3提供了用户可以轻松设置的属性来改变模型行为。考虑一下CsmaNetDevice的两个属性。Mtu和EncapsulationMode。Mtu属性表示设备的最大传输单元。这是该设备可以发送的最大的协议数据单元(PDU)的大小。

MTU在CsmaNetDevice中默认为1500字节。这个默认值对应于RFC894中的一个数字,“在以太网上传输IP数据报的标准”。这个数字实际上是由10Base5(全规格以太网)网络的最大数据包大小–1518字节得出的。如果你减去以太网数据包的DIX封装开销(18字节),你将得到1500字节的最大可能数据大小(MTU)。人们还可以发现,IEEE 802.3网络的MTU是1492字节。这是因为LLC/SNAP封装给数据包增加了额外的8个字节的开销。在这两种情况下,底层硬件只能发送1518字节,但数据大小不同。

为了设置封装模式,CsmaNetDevice提供了一个名为EncapsulationMode的属性,它可以取值为Dix或Llc。这些值分别对应于以太网和LLC/SNAP成帧。

如果将Mtu保持在1500字节,并将封装模式改为Llc,结果将是一个用LLC/SNAP构架封装1500字节PDU的网络,导致1526字节的数据包,这在许多网络中是非法的,因为它们每个数据包最多可传输1518字节。这很可能导致模拟结果与你所期望的现实情况有相当大的出入。

为了使情况复杂化,还存在巨型帧(1500 < MTU <= 9000字节)和超级巨型帧(MTU > 9000字节),它们没有得到IEEE的正式认可,但在一些高速(千兆)网络和网卡中可用。人们可以将封装模式设置为Dix,并将CsmaNetDevice上的Mtu属性设置为64000字节–即使相关的CsmaChannel DataRate被设置为每秒10兆比特。这基本上是一个以太网交换机的模型,它是由支持超级巨量数据报的1980年代风格的10Base5网络组成的吸血鬼。这当然不是曾经制造过的东西,也不可能被制造出来,但对你来说,配置它是相当容易的。

在前面的例子中,你用命令行创建了一个有100个Csma节点的模拟。你也可以很容易地创建一个有500个节点的模拟。如果你真的在模拟10Base5吸血式分接头网络,全规格的以太网电缆的最大长度是500米,最小分接头间距是2.5米。这意味着在一个真正的网络上只能有200个分接点。你也可以很容易地以这种方式建立一个非法网络。这可能会也可能不会产生有意义的模拟,这取决于你试图模拟的内容。

在NS-3和任何模拟器中,类似的情况会在很多地方发生。例如,你可能会以这样的方式定位节点,使它们在同一时间占据相同的空间,或者你可能会配置违反基本物理定律的放大器或噪声水平。

ns-3通常倾向于灵活性,许多模型将允许自由设置属性,而不试图强制执行任何任意的一致性或特定的基础规范。

从这里可以看出,NS-3将提供一个超级灵活的基础,供你实验。你应该理解你要求系统做什么,并确保你创建的模拟有一定的意义和价值。

Building a Wireless Network Topology

ns-3提供了一套802.11模型,试图提供802.11规范的精确MAC级实现和802.11a规范的 "不那么慢的 "PHY级模型。

// Default Network Topology
//
//   Wifi 10.1.3.0
//                 AP
//  *    *    *    *
//  |    |    |    |    10.1.1.0
// n5   n6   n7   n0 -------------- n1   n2   n3   n4
//                   point-to-point  |    |    |    |
//                                   ================
//                                     LAN 10.1.2.0

在左边挂一个无线网络。注意,这是一个默认的网络拓扑结构,因为你实际上可以改变在有线和无线网络上创建的节点数量。就像在第二个.cc脚本案例中一样,如果你改变nCsma,它将给你一些 "额外的 "CSMA节点。

同样地,你可以设置nWifi来控制模拟中创建多少个STA(站)节点。无线网络上总是会有一个AP(接入点)节点。默认情况下,有三个 "额外的 "CSMA节点和三个无线STA节点。

前面的与first,second相似,不赘述

接下来的代码构建了wifi设备和这些wifi节点之间的互连通道。首先,我们配置PHY和信道帮助器。

YansWifiChannelHelper channel = YansWifiChannelHelper::Default ();
YansWifiPhyHelper phy = YansWifiPhyHelper::Default ();

使用了默认的PHY层配置和信道模型,这些模型在API doxygen文档中记录了YansWifiChannelHelper::Default和YansWifiPhyHelper::Default方法。一旦这些对象被创建,我们就创建一个信道对象,并将其关联到我们的PHY层对象管理器,以确保所有由YansWifiPhyHelper创建的PHY层对象共享同一个底层信道,也就是说,它们共享同一个无线介质,可以进行通信和干扰。

phy.SetChannel (channel.Create ());

一旦PHY助手配置好了,我们就可以把重点放在MAC层。这里我们选择与非Qos的MAC一起工作。WifiMacHelper对象被用来设置MAC参数。

WifiHelper wifi;
wifi.SetRemoteStationManager ("ns3::AarfWifiManager");

WifiMacHelper mac;

SetRemoteStationManager方法告诉助手要使用的速率控制算法的类型。这里,它要求帮助器使用AARF算法–当然,细节可以在Doxygen中找到。

接下来,我们配置MAC的类型,以及我们要建立的基础设施网络的SSID,并确保我们的站点不进行主动探测。

Ssid ssid = Ssid ("ns-3-ssid");
mac.SetType ("ns3::StaWifiMac",
  "Ssid", SsidValue (ssid),
  "ActiveProbing", BooleanValue (false));

这段代码首先创建了一个802.11服务集标识符(SSID)对象,将用于设置MAC层实现的 "Ssid "属性的值。将由帮助程序创建的特定类型的MAC层由Attribute指定为 "ns3::StaWifiMac "类型。WifiMacHelper对象的 "QosSupported "属性默认设置为false。这两个配置的结合意味着接下来创建的MAC实例将是一个基础设施BSS(即有AP的BSS)中的非QoS非AP站(STA)。最后,"ActiveProbing "属性被设置为假。这意味着探针请求将不会由这个助手创建的MAC发送。

一旦在MAC层和PHY层完全配置了所有的站点特定参数,我们就可以调用我们现在熟悉的安装方法来创建这些站点的Wi-Fi设备。

NetDeviceContainer staDevices;
staDevices = wifi.Install (phy, mac, wifiStaNodes);

我们已经为所有的STA节点配置了Wi-Fi,现在我们需要配置AP(接入点)节点。

mac.SetType ("ns3::ApWifiMac",
             "Ssid", SsidValue (ssid));

WifiMacHelper要创建 "ns3::ApWifiMac "的MAC层,后者指定要创建一个配置为AP的MAC实例。我们不改变 "QosSupported "属性的默认设置

在创建的AP上禁用802.11e/WMM式QoS支持。

接下来的几行创建了单个AP,它与站台共享相同的PHY级属性集(和信道)。

NetDeviceContainer apDevices;
apDevices = wifi.Install (phy, mac, wifiApNode);

现在,我们要添加**移动性模型。**我们希望STA节点是移动的,在一个边界框内徘徊,我们使用MobilityHelper来使其变得简单。

首先,我们实例化一个MobilityHelper对象并设置一些控制 "位置分配器 "功能的属性。

MobilityHelper mobility;

mobility.SetPositionAllocator ("ns3::GridPositionAllocator",
  "MinX", DoubleValue (0.0),
  "MinY", DoubleValue (0.0),
  "DeltaX", DoubleValue (5.0),
  "DeltaY", DoubleValue (10.0),
  "GridWidth", UintegerValue (3),
  "LayoutType", StringValue ("RowFirst"));

这段代码告诉移动性助手使用一个二维网格来最初放置STA节点。请自由探索NS3::GridPositionAllocator类的Doxygen,看看到底在做什么。

mobility.SetMobilityModel ("ns3::RandomWalk2dMobilityModel",
  "Bounds", RectangleValue (Rectangle (-50, 50, -50, 50)));

RandomWalk2dMobilityModel,它让节点以随机的速度在一个边界框内沿随机的方向移动。

我们现在告诉MobilityHelper在STA节点上安装移动性模型。

mobility.Install (wifiStaNodes);

我们希望接入点在模拟过程中保持在一个固定的位置。我们通过将该节点的移动性模型设置为ns3::ConstantPositionMobilityModel来实现这一目的。

mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");
mobility.Install (wifiApNode);

我们现在有了我们的节点、设备和信道,并为Wi-Fi节点选择了移动性模型,但我们没有协议栈存在。就像我们以前多次做的那样,我们将使用InternetStackHelper来安装这些协议栈。

InternetStackHelper stack;
stack.Install (csmaNodes);
stack.Install (wifiApNode);
stack.Install (wifiStaNodes);

就像在second.cc示例脚本中一样,我们将使用Ipv4AddressHelper来为我们的设备接口分配IP地址。

首先,我们使用网络10.1.1.0为我们的两个点对点设备创建所需的两个地址。

然后我们使用网络10.1.2.0为CSMA网络分配地址,

然后我们从网络10.1.3.0为STA设备和无线网络的AP分配地址。

Ipv4AddressHelper address;

address.SetBase ("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer p2pInterfaces;
p2pInterfaces = address.Assign (p2pDevices);

address.SetBase ("10.1.2.0", "255.255.255.0");
Ipv4InterfaceContainer csmaInterfaces;
csmaInterfaces = address.Assign (csmaDevices);

address.SetBase ("10.1.3.0", "255.255.255.0");
address.Assign (staDevices);
address.Assign (apDevices);

我们把回声服务器放在文件开头的插图中 "最右边 "的节点上。

UdpEchoServerHelper echoServer (9);

ApplicationContainer serverApps = echoServer.Install (csmaNodes.Get (nCsma));
serverApps.Start (Seconds (1.0));
serverApps.Stop (Seconds (10.0));

把回声客户端放在我们创建的最后一个STA节点上,把它指向CSMA网络上的服务器。

UdpEchoClientHelper echoClient (csmaInterfaces.GetAddress (nCsma), 9);
echoClient.SetAttribute ("MaxPackets", UintegerValue (1));
echoClient.SetAttribute ("Interval", TimeValue (Seconds (1.0)));
echoClient.SetAttribute ("PacketSize", UintegerValue (1024));

ApplicationContainer clientApps =
  echoClient.Install (wifiStaNodes.Get (nWifi - 1));
clientApps.Start (Seconds (2.0));
clientApps.Stop (Seconds (10.0));

由于我们在这里建立了一个内部网络,我们需要启用内部网络路由,就像我们在second.cc示例脚本中做的那样。

Ipv4GlobalRoutingHelper::PopulateRoutingTables ();

那就是我们刚刚创建的模拟将永远不会 "自然 "停止。这是因为我们要求无线接入点产生信标。它将永远产生信标,这将导致模拟器事件被无限期地安排到未来,所以我们必须告诉模拟器停止,即使它可能有信标生成事件安排。下面这行代码告诉模拟器停止,这样我们就不会永远模拟信标并进入本质上的无尽循环。

Simulator::Stop (Seconds (10.0));

我们创建足够多的跟踪来覆盖所有三个网络。

pointToPoint.EnablePcapAll ("third");
phy.EnablePcap ("third", apDevices.Get (0));
csma.EnablePcap ("third", csmaDevices.Get (0), true);

这三行代码将在作为主干网的两个点对点节点上启动 pcap 追踪,在 Wi-Fi 网络上启动混杂(监控)模式追踪,并在 CSMA 网络上启动混杂追踪。

Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
'build' finished successfully (0.407s)
At time 2s client sent 1024 bytes to 10.1.2.4 port 9
At time 2.01796s server received 1024 bytes from 10.1.3.3 port 49153
At time 2.01796s server sent 1024 bytes to 10.1.3.3 port 49153
At time 2.03364s client received 1024 bytes from 10.1.2.4 port 9

第一条信息 “发送1024字节到10.1.2.4”,是UDP回声客户端向服务器发送一个数据包。在这种情况下,客户端是在无线网络上(10.1.3.0)。

第二条消息,“收到来自10.1.3.3的1024字节”,是来自UDP回波服务器,当它收到回波数据包时产生的。

最后一条消息,“收到来自10.1.2.4的1024字节”,是来自回声客户端,表明它已经收到了来自服务器的回声。

后tacp跟踪过程分析略

Queues in ns-3

ns-3中queueing disciplines 的选择会对性能产生很大的影响,对于用户来说,了解默认安装的内容以及如何改变默认值并观察性能是很重要的。

在架构上,ns-3将设备层与互联网主机的IP层或流量控制层分开。从最近的ns-3版本开始,出站的数据包在到达信道对象之前会穿越两个排队层。

遇到的第一个队列层(queueing layer)在ns-3中被称为 “流量控制层”;在这里,主动队列管理(RFC7567)和由于服务质量(QoS)的优先级通过使用队列规则以独立于设备的方式进行。

第二个队列层通常出现在NetDevice对象中。不同的设备(如LTE、Wi-Fi)对这些队列有不同的实现方式。这种两层方法反映了实践中的情况,(软件队列提供优先级,硬件队列特定于链接类型)。在实践中,它可能比这更复杂。例如,地址解析协议有一个小队列。Linux中的Wi-Fi有四层队列(https://lwn.net/Articles/705884/)。

流量控制层只有在设备队列满的时候被NetDevice通知才有效,这样流量控制层就可以停止向NetDevice发送数据包。否则,排队纪律的积压总是空的,它们是无效的。目前,流量控制,即通知流量控制层的能力,由以下NetDevice支持,它们使用队列对象(或队列子类的对象)来存储其数据包。

  • Point-To-Point
  • Csma
  • Wi-Fi
  • SimpleNetDevice

排队规则的性能受NetDevices使用的队列大小的影响很大。目前,ns-3中默认的队列没有针对配置的链路属性(带宽、延迟)进行自动调整,而且通常是最简单的变体(例如,带有落尾行为的FIFO调度)。然而,队列的大小可以通过启用BQL(字节队列限制)来动态调整,BQL是在Linux内核中实现的算法,用于调整设备队列的大小,在避免饥饿的同时对抗缓冲区的膨胀。目前,BQL由支持流量控制的NetDevices支持。通过ns-3模拟和实际实验,分析了设备队列的大小对排队规则有效性的影响。

拓展阅读

P. Imputato和S. Avallone。通过实验和模拟分析网络设备缓冲区对数据包调度器的影响。Simulation Modelling Practice and Theory, 80(Supplement C):1-18, January 2018. DOI: 10.1016/j.simpat.2017.09.008

P. Imputato and S. Avallone. An analysis of the impact of network device buffers on packet schedulers through experiments and simulations. Simulation Modelling Practice and Theory, 80(Supplement C):1–18, January 2018. DOI: 10.1016/j.simpat.2017.09.008

Available queueing models in ns-3

  • PFifoFastQueueDisc。默认最大尺寸为1000个数据包
  • FifoQueueDisc: 默认的最大尺寸是1000个数据包
  • RedQueueDisc: 默认最大尺寸为25个数据包
  • CoDelQueueDisc: 默认的最大尺寸是1500千字节
  • FqCoDelQueueDisc: 默认的最大尺寸是10024个数据包
  • PieQueueDisc: 默认的最大尺寸是25个数据包
  • MqQueueDisc。该队列盘的容量没有限制
  • TbfQueueDisc: 默认的最大容量是1000个数据包

默认情况下,当IPv4或IPv6地址被分配给与NetDevice相关的接口时,除非NetDevice上已经安装了队列纪律,否则会在NetDevice上安装pfifo_fast队列纪律。

在设备层,有特定的设备队列。

  • PointToPointNetDevice。默认配置(由帮助器设置)是安装一个默认大小的DropTail队列(100个数据包)。
  • CsmaNetDevice。默认配置(由帮助者设置)是安装一个默认大小(100个数据包)的DropTail队列。
  • WiFiNetDevice。默认配置是为非QoS站点安装一个默认大小(100个数据包)的DropTail队列,为QoS站点安装四个默认大小(100个数据包)的DropTail队列。
  • SimpleNetDevice。默认配置是安装一个默认大小的DropTail队列(100个数据包)。
  • LTENetDevice。排队发生在RLC层(RLC UM默认的缓冲区是10*1024字节,RLC AM没有缓冲区限制)。
  • UanNetDevice。在MAC层有一个默认的10个数据包队列

Changing from the defaults

  • The type of queue used by a NetDevice can be usually modified through the device helper:

    NodeContainer nodes;
    nodes.Create (2);
    
    PointToPointHelper p2p;
    p2p.SetQueue ("ns3::DropTailQueue", "MaxSize", StringValue ("50p"));
    
    NetDeviceContainer devices = p2p.Install (nodes);
    
  • The type of queue disc installed on a NetDevice can be modified through the traffic control helper:

    InternetStackHelper stack;
    stack.Install (nodes);
    
    TrafficControlHelper tch;
    tch.SetRootQueueDisc ("ns3::CoDelQueueDisc", "MaxSize", StringValue ("1000p"));
    tch.Install (devices);
    
  • BQL can be enabled on a device that supports it through the traffic control helper:

    InternetStackHelper stack;
    stack.Install (nodes);
    
    TrafficControlHelper tch;
    tch.SetRootQueueDisc ("ns3::CoDelQueueDisc", "MaxSize", StringValue ("1000p"));
    tch.SetQueueLimits ("ns3::DynamicQueueLimits", "HoldTime", StringValue ("4ms"));
    tch.Install (devices);
    

Tracing

Background

运行ns-3仿真的全部意义在于产生输出以供研究。

你有两种基本策略从ns-3获得输出:

  • 使用通用的预定义批量输出机制并解析其内容以提取有趣的信息;
  • 或者以某种方式开发一种输出机制,准确地传达(也许只有)想要的信息。

ns-3的跟踪系统的优点

使用预定义的批量输出机制的好处是不需要对ns-3做任何改动,但它可能需要编写脚本来解析和过滤感兴趣的数据。通常,PCAP或NS_LOG的输出信息是在模拟运行期间收集的,并分别通过脚本运行,使用grep、sed或awk来解析信息,将数据减少并转化为可管理的形式。必须编写程序来进行转换

首先,你可以通过只追踪你感兴趣的事件来减少你要管理的数据量(对于大型模拟,把所有的东西都转储到磁盘进行后处理会产生I/O瓶颈)。第二,如果你使用这种方法,你可以直接控制输出的格式,这样你就避免了用sed、awk、perl或python脚本进行后处理的步骤。如果你愿意**,你的输出可以直接被格式化为gnuplot可以接受的形式**,例如(也可参见GnuplotHelper)。你可以在核心部分添加钩子,然后由其他用户访问,但除非明确要求,否则不会产生任何信息。

Blunt Instruments

有很多方法可以从程序中获取信息。最直接的方法是直接将信息打印到标准输出

#include <iostream>
...
void
SomeFunction (void)
{
  uint32_t x = SOME_INTERESTING_VALUE;
  ...
  std::cout << "The value of x is " << x << std::endl;
  ...
}

从ns-3中获取信息的一个方法是解析现有的NS_LOG输出

你可以编辑ns-3的核心,并简单地将你感兴趣的信息添加到输出流中。现在,这当然比添加你自己的打印语句要好,因为它遵循了ns-3的编码惯例,并且有可能作为现有核心的一个补丁对其他人有用。

让我们随便选一个例子。如果你想给ns-3 TCP套接字(tcp-socket-base.cc)增加更多的日志记录,你可以在实现中添加一个新的消息下来。注意在TcpSocketBase::ProcessEstablished()中,没有关于在ESTABLISHED状态下接收SYN+ACK的日志信息。你可以简单地添加一个,改变代码。这里是原文。

/* Received a packet upon ESTABLISHED state. This function is mimicking the
    role of tcp_rcv_established() in tcp_input.c in Linux kernel. */
void
TcpSocketBase::ProcessEstablished (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);
  ...

  else if (tcpflags == (TcpHeader::SYN | TcpHeader::ACK))
    { // No action for received SYN+ACK, it is probably a duplicated packet
    }
  ...

To log the SYN+ACK case, you can add a new NS_LOG_LOGIC in the if statement body:

/* Received a packet upon ESTABLISHED state. This function is mimicking the
    role of tcp_rcv_established() in tcp_input.c in Linux kernel. */
void
TcpSocketBase::ProcessEstablished (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);
  ...
  else if (tcpflags == (TcpHeader::SYN | TcpHeader::ACK))
    { // No action for received SYN+ACK, it is probably a duplicated packet
      NS_LOG_LOGIC ("TcpSocketBase " << this << " ignoring SYN+ACK");
    }
  ...

这乍看起来相当简单和令人满意,但需要考虑的是,你将写代码来添加NS_LOG语句,你还必须写代码(如grep、sed或awk脚本)来解析日志输出,以便分离出你的信息。这是因为即使你对日志系统输出的内容有一些控制,你也只能控制到日志组件级别,通常是整个源代码文件。

如果你正在为一个现有的模块添加代码,你也将不得不忍受其他所有开发者都认为有趣的输出。你可能会发现,为了获得你所需要的少量信息,你可能不得不涉足大量的、你不感兴趣的无关信息。你可能被迫把巨大的日志文件保存到磁盘上,每当你想做什么的时候,就把它们处理成几行。

**由于NS_LOG输出的稳定性在NS-3中没有保证,**你可能会发现你所依赖的日志输出的部分在不同的版本中消失或改变。如果你依赖于输出的结构,你可能会发现其他的消息被添加或删除,这可能会影响你的解析代码。

最后,NS_LOG输出只在调试构建中可用,你无法从优化构建中获得日志输出,而优化构建的运行速度大约是两倍。依靠NS_LOG会带来性能上的损失。

由于这些原因,我们认为打印到std::cout和NS_LOG消息是快速和肮脏的方法,可以从ns-3中获得更多的信息,但不适合认真工作。

我们希望有一个稳定的设施,使用稳定的API,允许人们进入核心系统并只获得所需的信息。最好是能够做到这一点而不需要改变和重新编译核心系统。更好的是,当一个感兴趣的项目发生变化或一个有趣的事件发生时,系统会通知用户代码,这样用户就不必主动在系统中寻找东西了。

ns-3的跟踪系统就是按照这个思路设计的,它与属性和配置子系统很好地结合在一起,可以实现相对简单的使用场景。

Overview

ns-3追踪系统是建立在独立的追踪源和追踪汇的概念上的,同时还有连接源和汇的统一机制。

追踪源(Trace sources) 是可以对模拟中发生的事件发出信号的实体,并提供对有趣的基础数据的访问。例如,追踪源可以指示网络设备何时收到一个数据包,并为感兴趣的追踪汇提供对数据包内容的访问。追踪源还可以指示模型中发生有趣的状态变化时。例如,TCP模型的拥塞窗口是跟踪源的一个主要候选者。每当拥塞窗口发生变化时,连接的跟踪汇就会收到新旧数值的通知。

追踪源本身是没有用的;它们必须与其他代码片断相连接,这些代码片断实际上是用追踪源所提供的信息做一些有用的事情。

消费跟踪信息的实体被称为跟踪汇(trace sinks)。

跟踪源是数据的产生者,跟踪汇是消费者。这种明确的划分使得大量的跟踪源可以散布在系统中那些模型作者认为可能有用的地方。插入跟踪源会带来非常小的执行开销。一个跟踪源产生的跟踪事件可以有零个或多个消费者。我们可以把跟踪源看成是一种点对多点的信息链接。

Callback

ns-3中Callback系统的目标是允许一段代码调用一个函数(或C++中的方法),而没有任何特定的模块间的依赖。

把被调用函数的地址当作一个变量。这个变量被称为指针到函数的变量。函数和指针到函数之间的关系实际上与对象和指针到对象之间的关系没有什么不同。

基本上,一个跟踪汇是一个回调。当一个追踪汇表达了对接收追踪事件的兴趣时,它将自己作为一个回调加入到追踪源内部持有的回调列表中。当一个有趣的事件发生时,跟踪源会调用它的operator(…),提供零个或多个参数。operator(…)最终会游荡到系统中,做一些非常像你刚才看到的间接调用的事情,提供零个或多个参数,就像上面对pfi的调用向目标函数MyFunction传递一个参数一样。

追踪系统增加的重要区别是,每个追踪源都有一个内部的Callbacks列表。一个追踪源可以调用多个回调,而不是只做一个间接调用。当一个跟踪源表示对来自跟踪源的通知感兴趣时,它基本上只是安排将自己的函数添加到回调列表中。

fourth.cc

完整代码

#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h"
#include "ns3/trace-source-accessor.h"

#include <iostream>

using namespace ns3;

class MyObject : public Object
{
public:
  /**
   * Register this type.
   * \return The TypeId.
   */
  static TypeId GetTypeId (void)
  {
    static TypeId tid = TypeId ("MyObject")
      .SetParent<Object> ()
      .SetGroupName ("Tutorial")
      .AddConstructor<MyObject> ()
      .AddTraceSource ("MyInteger",
                       "An integer value to trace.",
                       MakeTraceSourceAccessor (&MyObject::m_myInt),
                       "ns3::TracedValueCallback::Int32")
    ;
    return tid;
  }

  MyObject () {}
  TracedValue<int32_t> m_myInt;
};

void
IntTrace (int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

int
main (int argc, char *argv[])
{
  Ptr<MyObject> myObject = CreateObject<MyObject> ();
  myObject->TraceConnectWithoutContext ("MyInteger", MakeCallback (&IntTrace));

  myObject->m_myInt = 1234;
}

由于跟踪系统是与属性(Attribute)集成的,而属性是与对象(Object)一起工作的,所以必须有一个ns-3对象来作为跟踪源的所在。下面的代码片段声明并定义了一个我们可以使用的简单对象。

class MyObject : public Object
{
public:
  static TypeId GetTypeId (void)
  {
    static TypeId tid = TypeId ("MyObject")
      .SetParent (Object::GetTypeId ())
      .SetGroupName ("MyGroup")
      .AddConstructor<MyObject> ()
      .AddTraceSource ("MyInteger",
                       "An integer value to trace.",
                       MakeTraceSourceAccessor (&MyObject::m_myInt),
                       "ns3::TracedValueCallback::Int32")
      ;
    return tid;
  }

  MyObject () {}
  TracedValue<int32_t> m_myInt;
};

AddTraceSource提供了用于通过配置系统将跟踪源连接到外部世界的 “钩子”(hooks)。

  • 第一个参数是该跟踪源的名称,这使得它在配置系统中可见。
  • 第二个参数是一个帮助字符串。现在看第三个参数,实际上是关注
  • 第三个参数的参数:&MyObject::m_myInt。这是正在被添加到类中的TracedValue;它总是一个类的数据成员。
  • (最后一个参数是TracedValue类型的类型定义的名称,是一个字符串)。这被用来生成正确的回调函数签名的文档,这对更多的一般类型的回调是很有用的)。

TracedValue<>声明提供了驱动回调过程的基础设施(The TracedValue<> declaration provides the infrastructure that drives the callback process)。任何时候,基础值被改变,TracedValue机制将提供该变量的旧值和新值,在本例中是一个int32_t值。这个TracedValue的跟踪汇出函数traceSink将需要以下签名(所有钩住这个追踪源的追踪汇必须有这个签名。)

void (* traceSink)(int32_t oldValue, int32_t newValue);

through fourth.cc we see:

void
IntTrace (int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

This is the definition of a matching trace sink.

它直接对应于回调函数的签名。一旦它被连接起来,每当TracedValue发生变化时,这个函数将被调用。

我们现在已经看到了跟踪源和跟踪汇。剩下的是连接源和汇的代码,这发生在main中。

int
main (int argc, char *argv[])
{
  Ptr<MyObject> myObject = CreateObject<MyObject> ();
  myObject->TraceConnectWithoutContext ("MyInteger", MakeCallback(&IntTrace));

  myObject->m_myInt = 1234;
}

TraceConnectWithoutContext,形成跟踪源和跟踪汇之间的连接。第一个参数是我们在上面看到的跟踪源名称 “MyInteger”。注意MakeCallback模板函数。

TraceConnect在你提供的函数和 "MyInteger "属性所指的被跟踪变量中的重载operator()之间建立关联。在建立这种联系后,跟踪源将 "启动 "你提供的回调函数。

由于m_myInt是一个TracedValue,这个操作符被定义为执行一个回调函数,该函数返回void,并接受两个整数值作为参数–一个旧值和一个有关整数的新值。这正是我们提供的回调函数的函数签名–IntTrace。

总结一下

跟踪源实质上是一个持有回调列表的变量。

追踪源是一个作为回调目标的函数。

属性和对象类型信息系统被用来提供一种连接跟踪源和跟踪汇的方法。

"击中(hitting) "一个跟踪源的行为是在跟踪源上执行一个操作符,该操作符会触发回调。这将导致在源上注册兴趣的跟踪汇回调被调用,参数由源提供。

If you now build and run this example,

$ ./waf --run fourth

you will see the output from the IntTrace function execute as soon as the trace source is hit:

Traced 0 to 1234

Connect with Config

TraceConnectWithoutContext调用实际上在系统中很少使用。

More typically, the Config subsystem is used to select a trace source in the system using what is called a Config path.(配置子系统被用来在系统中使用所谓的配置路径(Config path)来选择一个跟踪源。)

当我们用third.cc做实验时,我们钩住了 "CourseChange "事件

void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  Vector position = model->GetPosition ();
  NS_LOG_UNCOND (context <<
    " x = " << position.x << ", y = " << position.y);
}

当我们将 "CourseChange "追踪源连接到上述追踪汇时,当我们安排预先定义的追踪源和新的追踪汇之间的连接时,我们使用了一个Config路径来指定源。

std::ostringstream oss;
oss << "/NodeList/"
    << wifiStaNodes.Get (nWifi - 1)->GetId ()
    << "/$ns3::MobilityModel/CourseChange";

Config::Connect (oss.str (), MakeCallback (&CourseChange));

让我们试着理解一下有时被认为相对神秘的代码。为了便于讨论,假设GetId()返回的节点号为“7”。在这种情况下,上面的路径是

"/NodeList/7/$ns3::MobilityModel/CourseChange"

配置路径的最后一段必须是对象的属性。事实上,如果你手头有一个指向具有“CourseChange”属性的对象的指针,你可以像上一个例子中那样编写它。

现在你知道了,我们通常在NodeContainer中存储指向节点的指针。在第三节。例如,感兴趣的节点存储在wifiStaNodes节点容器中。实际上,在组合路径时,我们使用这个容器来获取一个Ptr,我们使用它来调用GetId()。我们可以使用这个Ptr直接调用Connect方法:

Ptr<Object> theObject = wifiStaNodes.Get (nWifi - 1);
theObject->TraceConnectWithoutContext ("CourseChange", MakeCallback (&CourseChange));

在third.cc, 我们实际上想要**一个额外的“上下文”与回调参数一起交付(**将在下面解释),这样我们就可以实际使用以下等效代码:

Ptr<Object> theObject = wifiStaNodes.Get (nWifi - 1);
theObject->TraceConnect ("CourseChange", MakeCallback (&CourseChange));

Config::ConnectWithoutContext和Config::Connect的内部代码实际上是找到一个Ptr,并在最低层调用相应的TraceConnect方法。

Config函数采用一个表示对象指针链的路径。路径的每一段对应于一个对象属性。

配置代码解析并“走”这条路径,直到它到达路径的最后一段。然后,它将最后一段解释为在路径上行走时找到的最后一个对象的属性。然后,配置函数对最终对象调用相应的TraceConnect或TraceConnectWithoutContext方法。

让我们更详细地了解一下,当沿着上述路径行走时会发生什么。

路径中的前导“/”字符指的是所谓的名称空间。配置系统中预定义的名称空间之一是“NodeList”,它是模拟中所有节点的列表。列表中的项目由列表中的索引引用,**因此“/NodeList/7”指的是在模拟过程中创建的节点列表中的第八个节点(**召回索引从0开始)。这个引用实际上是一个“`Ptr”,也是ns3::对象的一个子类。

在我们的示例中,正在行走的下一个路径段以“$”字符开头。这向配置系统表明该段是对象类型的名称,因此应该调用GetObject来查找该类型。事实证明,在第三种情况下使用了MobilityHelper。cc安排将移动模型聚合或关联到每个无线节点。当您添加“$”时,您要求的是另一个对象,该对象可能之前已聚合。您可以将其视为将指针从“/NodeList/7”指定的原始Ptr切换到其关联的移动性模型,即ns3::MobilityModel类型。如果您熟悉GetObject,我们已要求系统执行以下操作:

Ptr<MobilityModel> mobilityModel = node->GetObject<MobilityModel> ()

MobilityModel类定义了一个名为 "CourseChange "的属性。你可以通过查看 src/mobility/model/mobility-model.cc 中的源代码,并在你喜欢的编辑器中搜索 "CourseChange "来查看。你应该发现

.AddTraceSource ("CourseChange",
                 "The value of the position and/or velocity vector changed",
                 MakeTraceSourceAccessor (&MobilityModel::m_courseChangeTrace),
                 "ns3::MobilityModel::CourseChangeCallback")

在这一点上,它应该看起来非常熟悉。

如果你在mobility-model.h中寻找底层追踪变量的相应声明,你会发现

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

类型声明TracedCallback将m_courseChangeTrace标识为一个特殊的Callbacks列表,可以使用上面描述的Config函数来挂钩。回调函数签名的typedef也在头文件中定义。

typedef void (* CourseChangeCallback)(Ptr<const MobilityModel> * model);

MobilityModel类被设计成一个基类,为所有具体的子类提供一个公共接口。如果你向下搜索到文件的末尾,你会看到一个被定义为NotifyCourseChange()的方法。

void
MobilityModel::NotifyCourseChange (void) const
{
  m_courseChangeTrace(this);
}

派生类在做课程改变时都会调用这个方法以支持追踪。这个方法在底层的m_courseChangeTrace上调用operator(),反过来,它将调用所有注册的Callbacks,通过调用Config函数,调用所有对追踪源有兴趣的追踪汇。

因此,在我们看的第三个.cc例子中,每当安装的RandomWalk2dMobilityModel实例发生课程变化时,就会有一个NotifyCourseChange()的调用,该调用进入MobilityModel基类。如上所述,这将调用m_courseChangeTrace的operator(),而后者又会调用任何注册的跟踪汇。在这个例子中,唯一注册了兴趣的代码是提供配置路径的代码。因此,从第7个节点上钩的CourseChange函数将是唯一被调用的Callback。

我们从third.cc中看到的输出看起来像下面这样。

/NodeList/7/$ns3::MobilityModel/CourseChange x = 7.27897, y =
2.22677

输出的第一部分是背景。它是简单的路径,配置代码通过它找到跟踪源。在我们一直关注的案例中,系统中可能有任何数量的跟踪源,对应于任何数量的移动性模型的节点。需要有一些方法来识别哪一个跟踪源实际上是触发回调的那个。简单的方法是用Config::Connect连接,而不是Config::ConnectWithoutContext。

Finding Sources

  • 我知道模拟核心中一定有追踪源,但是我怎么才能找到哪些追踪源可供我使用?
  • 我找到了一个跟踪源,当我连接到它的时候,我怎么弄清楚要使用的配置路径?
  • 我找到了一个跟踪源和配置路径,我怎么弄清楚我的回调函数的返回类型和正式参数是什么?
  • 好吧,我把这些都打进去了,得到了这个令人难以置信的奇怪的错误信息,它到底是什么意思?
Available Sources

官方文档中NS-3中所有可用的跟踪源的列表。

Config Paths

如果你知道你对哪个对象感兴趣,该类的 "详细描述(Detailed Description”) "部分将列出所有可用的跟踪源。

例如,从 "所有跟踪源 "的列表开始,点击ns3::MobilityModel的链接,这将带你到MobilityModel类的文档。几乎在页面的顶部是对该类的一行简要描述,最后是一个链接 “更多…”。点击这个链接可以跳过API摘要,进入该类的 “详细描述”。在描述的末尾会有(最多)三个列表。

  • Config Paths: a list of typical Config paths for this class.
  • Attributes: a list of all attributes supplied by this class.
  • TraceSources: a list of all TraceSources available from this class.
Callback Signatures

最简单的方法是检查回调签名的类型定义,它在类的 "详细描述 "中跟踪源的 "回调签名 "中给出,如上所示。

Implementation

Traced Values

文件traced-value.h带来了所需的声明,以追踪服从于值语义的数据。一般来说,值语义只是意味着你可以传递对象本身,而不是传递对象的地址。我们将这一要求扩展到包括为普通数据(POD)类型预先定义的全套赋值式操作符。

operator= (assignment)
operator*=operator/=
operator+=operator-=
operator++ (both prefix and postfix)
operator-- (both prefix and postfix)
operator<<=operator>>=
operator&=operator|=
operator%=operator^=

你将能够追踪所有使用这些操作符所做的改变,并将其转化为具有值语义的C++对象。

TracedValue<>声明提供了基础结构,它重载了上面提到的操作符,并驱动回调过程。在使用上述任何一个操作符和TracedValue时,它将提供该变量的旧值和新值,在本例中是一个int32_t值。通过检查TracedValue的声明,我们知道追踪汇函数将有参数(int32_t oldValue,int32_t newValue)。TracedValue回调函数的返回类型总是无效的,所以预期的 sink function traceSink的回调签名将是。

void (* traceSink)(int32_t oldValue, int32_t newValue);

GetTypeId方法中的.AddTraceSource提供了用于通过配置系统将跟踪源连接到外部世界的 “挂钩”。我们已经讨论了AddTraceSource的前三个参数:配置系统的属性名称、一个帮助字符串和TracedValue类数据成员的地址。

最后一个字符串参数,即例子中的 “ns3::TracedValueCallback::Int32”,是回调函数签名的类型定义的名称。我们要求这些签名被定义,并将完全合格的类型名称交给AddTraceSource,这样API文档就可以将跟踪源与函数签名联系起来。对于TracedValue,签名是直截了当的;对于TracedCallbacks,我们已经看到API文档确实有帮助。

Real Example

TCP实验

首先要考虑的是我们要如何把数据弄出来。我们需要追踪的是什么?因此,让我们查阅 "所有跟踪源 "列表,看看我们有什么可以利用的。回顾一下,这是在ns-3 API文档中找到的。如果你滚动浏览这个列表,你最终会发现。

ns3::TcpSocketBase

  • CongestionWindow: The TCP connection’s congestion window(TCP连接的拥堵窗口)
  • SlowStartThreshold: TCP slow start threshold (bytes)(TCP慢速启动阈值(字节数))

Available Sources

ns-3的TCP实现(大部分)在src/internet/model/tcp-socket-base.cc文件中,而拥塞控制的变体在src/internet/model/tcp-bic.cc等文件中。如果你事先不知道这些,你可以使用递归grep(recursive grep)技巧。

$ find . -name '*.cc' | xargs grep -i tcp

调出TcpSocketBase的类文件,跳到TraceSources的列表中,你会发现

TraceSources

  • CongestionWindow: The TCP connection’s congestion window

    Callback signature: ns3::TracedValueCallback::Uint32

点击回调类型化的链接,我们看到了你现在知道要期待的签名。

typedef void(* ns3::TracedValueCallback::Int32)(int32_t oldValue, int32_t newValue)

Finding Examples

现在的首要任务是找到一些已经钩住 "CongestionWindow "跟踪源的代码,看看我们是否可以修改它。

$ find . -name '*.cc' | xargs grep CongestionWindow

寻找到examples/tcp/tcp-large-transfer.ccsrc/test/ns3tcp/ns3tcp-cwnd-test-suite.cc.

看一下这个test文件,内含

ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow",
  MakeCallback (&Ns3TcpCwndTestCase1::CwndChange, this));

它是一个由测试框架运行的脚本,所以我们可以直接把它拉出来,包在main里而不是DoRun里。与其一步一步地走下去,我们不如提供将这个测试移植到本地ns-3脚本的结果文件–examples/tutorial/fifth.cc。

Dynamic Trace Sources

**在使用任何类型的跟踪源之前,你必须明白:在尝试使用Config::Connect命令之前,你必须确保该命令的目标存在。这与说一个对象在试图调用它之前必须被实例化没有区别。**虽然这样说似乎很明显,但它确实让许多第一次尝试使用该系统的人感到困惑。

何NS-3脚本中都有三个基本的执行阶段。第一个阶段有时被称为 "配置时间 "或 “设置时间”,它存在于你的脚本的主要功能运行期间,但在Simulator::Run被调用之前。第二阶段有时被称为 “模拟时间”,存在于Simulator::Run积极执行其事件的时间段内。在它完成执行模拟后,Simulator::Run会将控制权返回给主函数。当这种情况发生时,脚本就进入了可以称为 “拆解阶段”,也就是在设置过程中创建的结构和对象被拆开并释放。

在尝试使用跟踪系统时,最常见的错误是假设在仿真时间内动态构建的实体在配置时间内是可用的。特别是,NS-3的Socket是一个动态对象,通常由应用程序创建,用于节点之间的通信。ns-3应用程序总是有一个 "开始时间 "和一个 "停止时间 "与它相关联。在绝大多数情况下,一个应用程序不会试图创建一个动态对象,直到它的StartApplication方法在某个 "开始时间 "被调用。这是为了确保在应用程序试图做任何事情之前,模拟已经完全配置好了(如果在配置时间内,它试图连接到一个还不存在的节点,会发生什么?) 因此,在配置阶段,如果其中一个是在模拟过程中动态创建的,你就不能将跟踪源连接到跟踪汇。

解决这个难题的两个办法是

  1. 创建一个仿真器事件,在动态对象创建后运行,并在该事件执行时钩住跟踪;
  2. 在配置时创建动态对象,然后钩住它,并将对象交给系统在模拟时使用。

fifth中使用第二个方法。这个决定要求我们创建MyApp应用程序,它的全部目的是接收一个Socket作为参数。

fifth.cc


#include <fstream>
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/internet-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/applications-module.h"

using namespace ns3;

NS_LOG_COMPONENT_DEFINE ("FifthScriptExample");

// ===========================================================================
//
//         node 0                 node 1
//   +----------------+    +----------------+
//   |    ns-3 TCP    |    |    ns-3 TCP    |
//   +----------------+    +----------------+
//   |    10.1.1.1    |    |    10.1.1.2    |
//   +----------------+    +----------------+
//   | point-to-point |    | point-to-point |
//   +----------------+    +----------------+
//           |                     |
//           +---------------------+
//                5 Mbps, 2 ms
//
//
// We want to look at changes in the ns-3 TCP congestion window.  We need
// to crank up a flow and hook the CongestionWindow attribute on the socket
// of the sender.  Normally one would use an on-off application to generate a
// flow, but this has a couple of problems.  First, the socket of the on-off 
// application is not created until Application Start time, so we wouldn't be 
// able to hook the socket (now) at configuration time.  Second, even if we 
// could arrange a call after start time, the socket is not public so we 
// couldn't get at it.
//
// So, we can cook up a simple version of the on-off application that does what
// we want.  On the plus side we don't need all of the complexity of the on-off
// application.  On the minus side, we don't have a helper, so we have to get
// a little more involved in the details, but this is trivial.
//
// So first, we create a socket and do the trace connect on it; then we pass 
// this socket into the constructor of our simple application which we then 
// install in the source node.
// ===========================================================================
//
class MyApp : public Application 
{
public:

  MyApp ();
  virtual ~MyApp();

  void Setup (Ptr<Socket> socket, Address address, uint32_t packetSize, uint32_t nPackets, DataRate dataRate);

private:
  virtual void StartApplication (void);
  virtual void StopApplication (void);

  void ScheduleTx (void);
  void SendPacket (void);

  Ptr<Socket>     m_socket;
  Address         m_peer;
  uint32_t        m_packetSize;
  uint32_t        m_nPackets;
  DataRate        m_dataRate;
  EventId         m_sendEvent;
  bool            m_running;
  uint32_t        m_packetsSent;
};

MyApp::MyApp ()
  : m_socket (0), 
    m_peer (), 
    m_packetSize (0), 
    m_nPackets (0), 
    m_dataRate (0), 
    m_sendEvent (), 
    m_running (false), 
    m_packetsSent (0)
{
}

MyApp::~MyApp()
{
  m_socket = 0;
}

void
MyApp::Setup (Ptr<Socket> socket, Address address, uint32_t packetSize, uint32_t nPackets, DataRate dataRate)
{
  m_socket = socket;
  m_peer = address;
  m_packetSize = packetSize;
  m_nPackets = nPackets;
  m_dataRate = dataRate;
}

void
MyApp::StartApplication (void)
{
  m_running = true;
  m_packetsSent = 0;
  m_socket->Bind ();
  m_socket->Connect (m_peer);
  SendPacket ();
}

void 
MyApp::StopApplication (void)
{
  m_running = false;

  if (m_sendEvent.IsRunning ())
    {
      Simulator::Cancel (m_sendEvent);
    }

  if (m_socket)
    {
      m_socket->Close ();
    }
}

void 
MyApp::SendPacket (void)
{
  Ptr<Packet> packet = Create<Packet> (m_packetSize);
  m_socket->Send (packet);

  if (++m_packetsSent < m_nPackets)
    {
      ScheduleTx ();
    }
}

void 
MyApp::ScheduleTx (void)
{
  if (m_running)
    {
      Time tNext (Seconds (m_packetSize * 8 / static_cast<double> (m_dataRate.GetBitRate ())));
      m_sendEvent = Simulator::Schedule (tNext, &MyApp::SendPacket, this);
    }
}

static void
CwndChange (uint32_t oldCwnd, uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
}

static void
RxDrop (Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
}

int 
main (int argc, char *argv[])
{
  CommandLine cmd;
  cmd.Parse (argc, argv);
  
  NodeContainer nodes;
  nodes.Create (2);

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

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

  Ptr<RateErrorModel> em = CreateObject<RateErrorModel> ();
  em->SetAttribute ("ErrorRate", DoubleValue (0.00001));
  devices.Get (1)->SetAttribute ("ReceiveErrorModel", PointerValue (em));

  InternetStackHelper stack;
  stack.Install (nodes);

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

  uint16_t sinkPort = 8080;
  Address sinkAddress (InetSocketAddress (interfaces.GetAddress (1), sinkPort));
  PacketSinkHelper packetSinkHelper ("ns3::TcpSocketFactory", InetSocketAddress (Ipv4Address::GetAny (), sinkPort));
  ApplicationContainer sinkApps = packetSinkHelper.Install (nodes.Get (1));
  sinkApps.Start (Seconds (0.));
  sinkApps.Stop (Seconds (20.));

  Ptr<Socket> ns3TcpSocket = Socket::CreateSocket (nodes.Get (0), TcpSocketFactory::GetTypeId ());
  ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow", MakeCallback (&CwndChange));

  Ptr<MyApp> app = CreateObject<MyApp> ();
  app->Setup (ns3TcpSocket, sinkAddress, 1040, 1000, DataRate ("1Mbps"));
  nodes.Get (0)->AddApplication (app);
  app->SetStartTime (Seconds (1.));
  app->SetStopTime (Seconds (20.));

  devices.Get (1)->TraceConnectWithoutContext ("PhyRxDrop", MakeCallback (&RxDrop));

  Simulator::Stop (Seconds (20));
  Simulator::Run ();
  Simulator::Destroy ();

  return 0;
}


class MyApp : public Application
{
public:

  MyApp ();
  virtual ~MyApp();

  void Setup (Ptr<Socket> socket, Address address, uint32_t packetSize,
    uint32_t nPackets, DataRate dataRate);

private:
  virtual void StartApplication (void);
  virtual void StopApplication (void);

  void ScheduleTx (void);
  void SendPacket (void);

  Ptr<Socket>     m_socket;
  Address         m_peer;
  uint32_t        m_packetSize;
  uint32_t        m_nPackets;
  DataRate        m_dataRate;
  EventId         m_sendEvent;
  bool            m_running;
  uint32_t        m_packetsSent;
};

MyApp类继承于ns-3的Application类。MyApp类有义务覆盖StartApplication和StopApplication方法。当MyApp在模拟过程中需要开始和停止发送数据时,这些方法会被自动调用。

如果你对继承的内容感兴趣,可以看看 src/network/model/application.h。

一些深入了解内容

开始泵送事件(pumping events)的最常见方式是启动一个应用程序。

我们想让模拟器自动调用我们的应用程序,告诉它们何时开始和停止。在MyApp的例子中,它继承了Application类,并重写了StartApplication和StopApplication。这些函数将在适当的时候被模拟器调用。在MyApp的例子中,你会发现MyApp::StartApplication在套接字上做初始的Bind和Connect,然后通过调用MyApp::SendPacket开始数据流。MyApp::StopApplication通过取消任何未决的发送事件来停止生成数据包,然后关闭套接字。

ApplicationContainer apps = ...
apps.Start (Seconds (1.0));
apps.Stop (Seconds (10.0));

如果你看一下 src/network/model/application.cc,你会发现 Application 的 SetStartTime 方法只是设置成员变量 m_startTime,SetStopTime 方法只是设置 m_stopTime。

是要知道系统中所有节点的全局列表。每当你在模拟中创建一个节点,这个节点的指针就会被添加到全局NodeList中。

请看 src/network/model/node-list.cc 并搜索 NodeList::Add。公有的静态实现调用了一个私有的实现,叫做NodeListPriv::Add。这在ns-3中是一个比较常见的idom。所以,看一下NodeListPriv::Add。在那里你会发现。

Simulator::ScheduleWithContext (index, TimeStep (0), &Node::Initialize, node);

每当在模拟中创建一个节点时,作为一个副作用,会为你安排对该节点的Initialize方法的调用,该调用发生在时间0。

它可以被理解为对节点的一个信息性调用,告诉它仿真已经开始,而不是告诉节点开始做什么的行动调用。

NodeList::Add间接地在零点安排了对Node::Initialize的调用,以告知一个新的节点,模拟已经开始。然而,如果你在src/network/model/node.h中寻找,你不会发现一个叫做Node::Initialize的方法。事实证明,Initialize方法是继承自Object类。系统中的所有对象都可以在模拟开始时得到通知,而Node类的对象只是这些对象中的一种。

src/core/model/object.cc,搜索Object::Initialize。这段代码并不像你想象的那样简单,因为ns-3对象支持聚合。Object::Initialize中的代码会循环浏览所有被聚合在一起的对象,并调用它们的DoInitialize方法。这是在ns-3中非常常见的另一个习惯,有时被称为 “模板设计模式”:一个公共的非虚拟API方法,在不同的实现中保持不变,并且调用一个私有的虚拟实现方法,该方法被子类继承和实现。这些名称通常是公共API的MethodName和私有API的DoMethodName这样的东西。

看Application::DoInitialize被调用时会发生什么。看一下 src/network/model/application.cc,你会发现。

void
Application::DoInitialize (void)
{
  m_startEvent = Simulator::Schedule (m_startTime, &Application::StartApplication, this);
  if (m_stopTime != TimeStep (0))
    {
      m_stopEvent = Simulator::Schedule (m_stopTime, &Application::StopApplication, this);
    }
  Object::DoInitialize ();
}

当你实现一个ns-3应用程序时,你的新应用程序继承自Application类。你重写StartApplication和StopApplication方法,并提供机制来启动和停止数据从你的新应用程序中流出。当一个节点在模拟中被创建时,它被添加到一个全局NodeList中。将一个节点添加到NodeList的行为会导致一个模拟器事件被安排在零点,当模拟开始时,会调用新添加的节点的Node::Initialize方法。由于Node继承自Object,这就调用了Node的Object::Initialize方法,而Node又调用了聚集在Node上的所有Object的DoInitialize方法(想想移动模型)。由于Node对象已经覆盖了DoInitialize,所以当模拟开始时,该方法被调用。Node::DoInitialize方法调用节点上所有应用程序的初始化方法。由于应用程序也是对象,这导致Application::DoInitialize被调用。当Application::DoInitialize被调用时,它为应用程序的StartApplication和StopApplication调用安排事件。这些调用被设计用来启动和停止应用程序的数据流。

The MyApp Application
Trace Sinks
Main Program

Running fifth.cc

$ ./waf --run fifth
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone-dev/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone-dev/ns-3-dev/build'
'build' finished successfully (0.684s)
1       536
1.0093  1072
1.01528 1608
1.02167 2144
...
1.11319 8040
1.12151 8576
1.12983 9112
RxDrop at 1.13696
...

让我们把这些输出重定向到一个叫做cwnd.dat的文件。

$ ./waf --run fifth > cwnd.dat 2>&1

现在在你喜欢的编辑器中编辑 “cwnd.dat”,删除waf构建状态和drop行,只留下追踪的数据

现在你可以运行gnuplot(如果你安装了它)并告诉它生成一些漂亮的图片。

$ gnuplot
gnuplot> set terminal png size 640,480
gnuplot> set output "cwnd.png"
gnuplot> plot "cwnd.dat" using 1:2 title 'Congestion Window' with linespoints
gnuplot> exit

_images/cwnd.png

Using Mid-Level Helpers

我们刚刚花了很多时间来实现一个例子,这个例子展示了我们想要用ns-3追踪系统来解决的所有问题!你是对的。你是对的。但是,请容忍我们。我们还没有完成。

我们想做的最重要的事情之一就是要有能力轻松地控制模拟的输出量;而且我们还想把这些数据保存到一个文件里,这样我们以后就可以参考了。我们可以使用ns-3中提供的中级跟踪帮助器来完成这个任务,并完成图片。

让我们来看看从第五.cc到第六.cc所需的改动。你会发现我们改变了跟踪汇的签名,并在每个汇中添加了一行,将跟踪的信息写入代表文件的流(stream)中。

Sixth.cc

我们想看一下ns-3 TCP拥塞窗口的变化。 我们需要启动一个流,并在发送方的套接字上钩住拥塞窗口属性。 通常情况下,我们会使用一个开关程序来生成一个流,但这有几个问题。 首先,开关应用程序的套接字直到应用程序启动时才被创建,所以我们无法在配置时钩住套接字(现在)。 第二,即使我们能在启动时间后安排一个调用,套接字也不是公开的,所以我们无法得到它。
所以,我们可以做一个简单的开关程序,做我们想做的。 从正面看,我们不需要所有复杂的开关机应用程序。 缺点是,我们没有一个助手,所以我们必须在细节上多花点心思,但这是微不足道的。
因此,首先,我们创建一个套接字,并对其进行跟踪连接;然后我们将这个套接字传递给我们的简单应用程序的构造函数,然后我们将其安装在源节点中。

后略

Trace Helpers

ns-3追踪器提供了一个丰富的环境来配置和选择不同的追踪事件并将其写入文件。在以前的章节中,主要是构建拓扑结构,我们已经看到了几种不同的跟踪帮助器方法,这些方法是为在其他(设备)帮助器中使用的。

暂略

Summary

ns-3包括一个极其丰富的环境,允许用户在几个层次上定制可以从模拟中提取的信息种类。

有高水平的辅助功能,允许用户简单地控制预定义输出的收集,以达到精细的粒度。还有中级的辅助功能,允许更复杂的用户自定义信息的提取和保存方式;还有低级的核心功能,允许专家用户改变系统,以更高层次的用户可以立即使用的方式来展示新的和以前未提取的信息。

这是一个非常全面的系统,我们意识到这是一个需要消化的东西,特别是对于新用户或那些对C++及其习语不熟悉的用户。我们确实认为跟踪系统是ns-3的一个非常重要的部分,因此建议尽可能地熟悉它。一旦你掌握了跟踪系统,理解ns-3系统的其他部分可能就很简单了。

Conclusion

Closing

ns-3是一个庞大而复杂的系统。在一个小的教程中,不可能涵盖所有你需要知道的东西。我们鼓励想了解更多信息的读者阅读以下的附加文档。

  • The ns-3 manual
  • The ns-3 model library documentation
  • The ns-3 Doxygen (API documentation)
  • The ns-3 wiki