ns-3 教程 —— 调整(Tweaking)

来源:互联网 发布:网络没问题游戏上不去 编辑:程序博客网 时间:2024/05/22 14:43

使用日志模块

在过 first.cc 脚本时,我们已经大致看了一下 ns-3 的日志模块。现在让我们更加深入地了解一下并看一看这个日志子系统被设计覆盖用例的种类。

日志概述

很多大型系统支持某种消息记录工具,NS-3也不例外。在某些情况下,只有错误信息被记录到“操作员控制台”(在基于 Unix 的系统中,这是典型的 stderr —— 标准错误)。在其他系统中,可以输出警告消息和更详细的信息消息(informational message)。在某些情况下,日志记录工具用于输出调试消息,可以快速将输出转换到 blur。

ns-3 的观点是所有的冗长级别都是有用的,我们提供了一个可选的、多级的消息记录方法。可以完全禁用日志记录、在组件到组件的基础上启用或全局启用,并提供可选的冗长级别。ns-3 的日志模块提供了一个简单的、相对易用的方式从你的仿真中来获得有用的信息。

你应该明白我们确实提供了一个通用的机制——跟踪——从模型中获取数据,这些数据应该优先于模拟输出(参见教程的 Using the Tracing System 部分了解跟踪系统的更多细节)。日志记录应该优先用于调试信息、警告、错误消息、或任何时候你想轻松地从你的脚本或模型获得的快速消息。

下面是定义在系统中的按递增冗长排列的日志消息:

  • LOG_ERROR —— 记录错误消息(关联宏:NS_LOG_ERROR);
  • LOG_WARN —— 记录警告消息(关联宏:NS_LOG_WARN);
  • LOG_DEBUG —— 记录相对罕见的临时调试消息(关联宏:NS_LOG_DEBUG);
  • LOG_INFO —— 记录关于程序进度的信息消息(关联宏:NS_LOG_INFO);
  • LOG_FUNCTION —— 记录描述每个函数调用的消息(两个关联的宏:NS_LOG_FUNCTION,用于成员函数,NS_LOG_FUNCTION_NOARGS,用于静态函数);
  • LOG_LOGIC —— 记录描述函数内逻辑流消息(关联宏:NS_LOG_LOGIC);
  • LOG_ALL —— 记录上面提到的所有内容(没有相关联的宏)。

对于一个 LOG_TYPE 还有一个 LOG_LEVEL_TYPE(如果使用它的话)除了会启动他自身以外还会启用所有它上面的级别的日志(这就造成了 LOG_ERROR 和 LOG_LEVEL_ERROR 是功能等价的,LOG_ALL 和 LOG_LEVEL_ALL 也是功能等价的)。例如,启用 LOG_INFO 只会启用由 NS_LOG_INFO 宏提供的消息,而启用 LOG_LEVEL_INFO 则会启用由 NS_LOG_DEBUG、NS_LOG_WARN 和 NS_LOG_ERROR 宏提供的消息。

我们还提供无条件日志宏,无论日志级别和组件选择是怎样的其始终显示。

  • NS_LOG_UNCOND —— 无条件地记录关联的消息(无关联的日志级别)。

每个级别可以单独地或叠加地被请求,日志记录可以使用 shell 环境变量(NS_LOG)来设置也可以通过日志记录系统函数来调用。正如本教程前面所述,日志系统有 Doxygen 文档,如果你还没有阅读 Logging Module 的文档,现在正是阅读此文档的好时机。

现在,你已经非常详细地阅读了该文档,让我们用这些知识在 scratch/myfirst.cc 示例脚本上获取一些有趣的信息吧。

启用日志记录

让我们用 NS_LOG 环境变量来启用更多的日志记录。首先,要找准我们的位置,去运行最后的脚本吧,就像你之前做的一样。

$ ./waf --run scratch/myfirst

你应该看到现在非常熟悉的输出:

$ 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.2Received 1024 bytes from 10.1.1.1Received 1024 bytes from 10.1.1.2

事实上,你上面看到的 “Sent” 和 “Received” 消息是来自 UdpEchoClientApplicationUdpEchoServerApplication 的日志消息。我们可以通过 NS_LOG 环境变量来设置客户端应用日志记录级别以要求其打印更多信息。

在这里我将假设你会使用一个类 sh 的 shell 它使用 “VARIABLE=value” 语法。如果你使用类 csh 的 shell,那么你必须将语法转换为这种 shell 所需的 “setenv VARIABLE value” 语法。

现在,UDP echo 客户端应用正在对 scratch/myfirst.cc 中的下面一行做出响应:

LogComponentEnable("UdpEchoClientApplication", LOG_LEVEL_INFO);

这行代码启用了 LOG_LEVEL_INFO 级别的日志记录。当我们传日志记录级别标志时,我们实际上启用了给定级别以及更低级的日志记录。在这里我们启用了 NS_LOG_INFONS_LOG_DEBUGNS_LOG_WARNNS_LOG_ERROR。我们无需改动脚本和重编译就可以增加日志记录级别并获取更多信息,只需像如下所示一样设置 NS_LOG 环境变量:

$ export NS_LOG=UdpEchoClientApplication=level_all

其设置 shell 环境变量 NS_LOG 为如下字符,

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.2Received 1024 bytes from 10.1.1.1UdpEchoClientApplication:HandleRead(0x6241e0, 0x624a20)Received 1024 bytes from 10.1.1.2UdpEchoClientApplication:StopApplication()UdpEchoClientApplication:DoDispose()UdpEchoClientApplication:~UdpEchoClient()

应用提供的附加调试信息来自 NS_LOG_FUNCTION 级别日志记录。在脚本执行期每当函数被调用时都会显示。通常,在成员函数中首选使用(至少) NS_LOG_FUNCTION(this)。而仅在静态函数中使用 NS_LOG_FUNCTION_NOARGS()。但是请注意,ns-3 系统并没有要求模型必须支持任何特定的日志功能。到底记录多少信息取决于模型开发者。在 echo 应用情景中,大量的日志记录是可行的。

你现在可以看到应用的函数调用日志。如果你仔细观察,你会发现字符串 UdpEchoClientApplication 和方法名之间并不是你所期望的 C++ 范围操作符(::)而是一个冒号。这里是有意这么做的。

UdpEchoClientApplication 实际上并不是类名,而是日志记录组件名。当源文件和类之间存在一对一的对应关系时这通常是类名,但是你应该理解它实际上不是类名。并且在这里用单冒号而不是双冒号这种相对微妙的方式提醒你在概念上将日志组件名与类名分开。

事实证明在某些情况下,可能很难确定是哪个方法生成日志消息的。 如果你看上面的文本,你可能好奇字符串“ Received 1024 bytes from 10.1.1.2 ”是哪个方法生成的。你可以通过用 prefix_funcNS_LOG 环境变量中做或运算解决此问题。如下所示:

$ export 'NS_LOG=UdpEchoClientApplication=level_all|prefix_func'

注意,引号是必需的,因为我们用来表示 OR 操作的垂直条(|)也是 Unix 管道连接器(pipe connector)。

现在,如果运行脚本,你将会看到每条信息都会以其组件名为前缀(prefix)。

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.2Received 1024 bytes from 10.1.1.1UdpEchoClientApplication:HandleRead(0x6241e0, 0x624a20)UdpEchoClientApplication:HandleRead(): Received 1024 bytes from 10.1.1.2UdpEchoClientApplication:StopApplication()UdpEchoClientApplication:DoDispose()UdpEchoClientApplication:~UdpEchoClient()

你现在可以看到所有来自 UDP echo 客户端的消息都被如此标记。消息“ Received 1024 bytes from 10.1.1.2 ”现在清楚地标记为来自 echo 客户端。剩余的消息肯定是来自 echo 服务器端。我们通过在环境变量中输入由冒号分隔的组件列表来启用该组件。

$ export 'NS_LOG=UdpEchoClientApplication=level_all|prefix_func:               UdpEchoServerApplication=level_all|prefix_func'

警告:你需要移除上面样例文本中 : 后的换行符,在这里仅仅是由于文档格式的需要而添加的。

现在,如果运行脚本,服务器端和客户端的日志消息你都可以看到了。你可能会看到这在调试问题非常有用。

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.2UdpEchoServerApplication:HandleRead(): Received 1024 bytes from 10.1.1.1UdpEchoServerApplication:HandleRead(): Echoing packetUdpEchoClientApplication:HandleRead(0x624920, 0x625160)UdpEchoClientApplication:HandleRead(): Received 1024 bytes from 10.1.1.2UdpEchoServerApplication:StopApplication()UdpEchoClientApplication:StopApplication()UdpEchoClientApplication:DoDispose()UdpEchoServerApplication:DoDispose()UdpEchoClientApplication:~UdpEchoClient()UdpEchoServerApplication:~UdpEchoServer()

有时能够看到生成日志消息的仿真时间也是有用的。你可以通过在 prefix_time 位做或运算来做到这一点。

$ 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.22.00369s UdpEchoServerApplication:HandleRead(): Received 1024 bytes from 10.1.1.12.00369s UdpEchoServerApplication:HandleRead(): Echoing packet2.00737s UdpEchoClientApplication:HandleRead(0x624290, 0x624ad0)2.00737s UdpEchoClientApplication:HandleRead(): Received 1024 bytes from 10.1.1.210s UdpEchoServerApplication:StopApplication()10s UdpEchoClientApplication:StopApplication()UdpEchoClientApplication:DoDispose()UdpEchoServerApplication:DoDispose()UdpEchoClientApplication:~UdpEchoClient()UdpEchoServerApplication:~UdpEchoServer()

你可以看到,对 UdpEchoServer 的构造函数在仿真时间 0 秒时被调用。这实际上是发生在仿真开始之前,但时间被显示为 0 秒。对于 UDPEchoClient 也是如此。

回想一下,scratch/first.cc 脚本在进入仿真 1 秒时启动了 echo server 应用。你可以看到服务器的 StartApplication 方法事实上在 1 秒时被调用。你还可以看到 echo 客户端应用(正如我们在脚本中要求的那样)在仿真时间 2 秒启动。

现在,你可以在客户端的 ScheduleTransmit 调用中跟踪仿真进程,该调用在 echo server 应用中调用 SendHandleRead 回调。注意,通过点对点链路发送的数据包实耗时间为 3.69 毫秒。echo 服务器记录的消息告诉你他们已经 echo 了这个数据包,经过信道延时之后 echo 客户端用它的 HandleRead 方法接收到了该包。

在仿真的表面之下还发生了很多你没看见的,你可以通过启用系统的所有日志组件来了解全部过程。将 NS_LOG 变量设置如下:

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

上面的星号是日志组件通配符。这将会启用仿真中所使用的所有组件的所有日志记录。我不在这里重现输出了(这里,对于单个数据包 echo 它就会产生 1265 行输出),如果你喜欢的话,你可以重定向这个信息到一个文件,然后通过你最喜欢的编辑器打开:

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

当我遇到问题而又不知道到底问题出在什么地方时,我个人倾向于使用这个异常详细版本的日志。我可以很容易地跟踪代码的进度,而不必在调试器中设置断点和逐步执行代码。我可以在我最喜欢的编辑器中编辑该输出,搜索我所期望的错误,看看到底哪个地方跟我预先设想的不一样。当我对于出现了什么问题有一个大致的想法时,我转换到调试器对该问题进行细粒度地检查。当你的脚本运行结果完全超出预期时,这种输出可能特别有用。如果使用调试器逐步调试可能会完全错过意外的偏差。记录偏差使它迅速可见。

添加日志到你的代码

你可以通过几个宏来调用日志组件向仿真添加新的日志记录。让我们用 scratch 目录下的 myfirst.cc 脚本来演示。

回想一下,我们已经在该脚本中定义了一个日志组件:

NS_LOG_COMPONENT_DEFINE ("FirstScriptExample");

现在你已经知道可以通过设置 NS_LOG 环境变量为各种等级来启用各种日志纪录组件。让我们继续向脚本中添加一些日志记录。用于添加一条信息级别日志消息的宏是 NS_LOG_INFO。继续,(在我们开始创建节点之前)添加一个宏,告诉你脚本是“创建拓扑”。这是在这个代码片段中完成的,

打开 scratch/myfirst.cc ,在下面这些行之前,

NodeContainer nodes;nodes.Create (2);

用你最喜欢的编辑器添加下面这一行,

NS_LOG_INFO ("Creating Topology");

现在,用 waf 构建脚本并清理 NS_LOG 变量以关闭之前启用的日志记录洪流:

$ ./waf$ export NS_LOG=

现在,如果运行脚本,

$ ./waf --run scratch/myfirst

你将不会看到新的消息,因为其相关的日志记录组件(FirstScriptExample)并未启用。为了能看到消息,你需要将 FirstScriptExample 日志记录组件的级别设置为比 NS_LOG_INFO 更高或相等。如果你只想看到这个特定级别的日志记录,可以通过如下操作启用它,

$ export NS_LOG=FirstScriptExample=info

如果现在运行脚本,将看到新的“创建拓扑”日志消息,

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)Creating TopologySent 1024 bytes to 10.1.1.2Received 1024 bytes from 10.1.1.1Received 1024 bytes from 10.1.1.2

使用命令行参数

重写默认属性

另一种不通过编辑和 build 来改变 ns-3 脚本行为的方式是通过命令行参数。我们提供了一种机制来解析命令行参数,并根据这些参数自动设置局部变量和全局变量。

使用命令行参数系统的第一步是声明命令行解析器。(在你的主程序中)这是很简单的,如下代码,

intmain (int argc, char *argv[]){  ...  CommandLine cmd;  cmd.Parse (argc, argv);  ...}

这简单的两行代码片段实际上是非常有用的。它打开了 ns-3 全局变量和属性系统的大门。在 scratch/myfirst.cc 脚本的 main 开头添加这两行、构建脚本并运行它,但通过如下方式请求帮助:

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

这将要求 Waf 运行脚本并传命令行参数 --PrintHelp 到脚本。引号是用来决定哪个程序获取哪个参数的。命令行解析器现在可以看到 --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.

注意 --PrintAttributes 选项。在我们分析 first.cc 脚本时,我们就提到过属性系统。我们看了下面几行代码,

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

并提到, 数据速率实际上是 PointToPointNetDevice属性 。让我们使用命令行参数解析器来看看 PointToPointNetDevice 的属性。 帮助列表告诉我们说应该提供一个 typeid。这对应于属性所属类的类名。 在本例中它是 ns3::PointToPointNetDevice。 继续输入,

$ ./waf --run "scratch/myfirst --PrintAttributes=ns3::PointToPointNetDevice"

该系统将打印出这类网络设备的所有属性。在属性之间你会看到,

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

这是一个当系统中创建 PointToPointNetDevice 时使用的缺省值。在上面,我们通过设置 PointToPointHelperAttribute 值重写了该默认值。通过从 scratch 目录下的 myfirst.cc 脚本中删除 SetDeviceAttribute 调用和 SetChannelAttribute 调用来对点对点设备和信道使用缺省值。

你的脚本现在只应声明该 PointToPointHelper 而不做任何设置操作,如下所示:

...NodeContainer nodes;nodes.Create (2);PointToPointHelper pointToPoint;NetDeviceContainer devices;devices = pointToPoint.Install (nodes);...

现在,让我们继续。用 Waf 编译该脚本,并回过头来启用来自 UDP echo 服务器的日志记录同时打开时间前缀。

$ export 'NS_LOG=UdpEchoServerApplication=level_all|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.405s)0s UdpEchoServerApplication:UdpEchoServer()1s UdpEchoServerApplication:StartApplication()Sent 1024 bytes to 10.1.1.22.25732s Received 1024 bytes from 10.1.1.12.25732s Echoing packetReceived 1024 bytes from 10.1.1.210s UdpEchoServerApplication:StopApplication()UdpEchoServerApplication:DoDispose()UdpEchoServerApplication:~UdpEchoServer()

回想一下,我们最后一次看到 echo 服务器接收到数据包的仿真时间是在 2.00369 秒。

2.00369s UdpEchoServerApplication:HandleRead(): Received 1024 bytes from 10.1.1.1

现在它在 2.25732 秒接收数据包。这是因为我们将 PointToPointNetDevice 的数据速率从 5 兆每秒降到了它默认的 32768 bit 每秒。

如果我们通过命令行提供一个新的 DataRate,我们可以再次加速仿真。根据帮助项目的提示,我们可以这么做:

$ ./waf --run "scratch/myfirst --ns3::PointToPointNetDevice::DataRate=5Mbps"

这会将 数据速率 属性 的默认值设置回 5 兆比特每秒。你对结果感到惊讶吗? 事实证明,为了获得脚本的原始行为,我们需要将信道延时设为光速,我们像之前对网络设备做的那样向命令行系统要求帮助,打印出信道的属性

$ ./waf --run "scratch/myfirst --PrintAttributes=ns3::PointToPointChannel"

我们发现信道的 延时 属性 以下述方式设置:

--ns3::PointToPointChannel::Delay=[0ns]:  Transmission delay through the channel

然后,我们可以通过命令行系统设置这两个默认值,

$ ./waf --run "scratch/myfirst  --ns3::PointToPointNetDevice::DataRate=5Mbps  --ns3::PointToPointChannel::Delay=2ms"

当我们在脚本中明确设置了 DataRateDelay 时,我们恢复了我们拥有的时间:

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)0s UdpEchoServerApplication:UdpEchoServer()1s UdpEchoServerApplication:StartApplication()Sent 1024 bytes to 10.1.1.22.00369s Received 1024 bytes from 10.1.1.12.00369s Echoing packetReceived 1024 bytes from 10.1.1.210s UdpEchoServerApplication:StopApplication()UdpEchoServerApplication:DoDispose()UdpEchoServerApplication:~UdpEchoServer()

注意,服务器再次在 2.00369 秒接收到该数据包。我们实际上可以用这种方法在脚本中设置任何这样的属性。特别的,我们可以将 UdpEchoClientMaxPackets 属性设置为其他值而不是 1。

你会怎么办呢?试试看。记住,你必须注释掉我们重写默认属性的地方并且在脚本中明确设置 MaxPackets。然后你必须重构脚本。您还必须找到使用命令行帮助工具设置新的默认属性值的语法。一旦你搞定了这些,你就应该能够控制从命令行 echo 的数据包数量。因为我们是好人,我们会告诉你你的命令行应该看起来像这样:

$ ./waf --run "scratch/myfirst  --ns3::PointToPointNetDevice::DataRate=5Mbps  --ns3::PointToPointChannel::Delay=2ms  --ns3::UdpEchoClient::MaxPackets=2"

一个问题很自然地冒出来了,我们怎么知道存在哪些属性?再次说一下,命令行帮助工有此功能。如果我们要求命令行帮助,我们应该看到:

$ ./waf --run "scratch/myfirst --PrintHelp"myfirst [Program Arguments] [General Arguments]General Arguments:  --PrintGlobals:              Print the list of globals.  --PrintGroups:               Print the list of groups.  --PrintGroup=[group]:        Print all TypeIds of group.  --PrintTypeIds:              Print all TypeIds.  --PrintAttributes=[typeid]:  Print all attributes of typeid.  --PrintHelp:                 Print this help message.

如果选择 PrintGroups 参数,你会看到所有已注册 TypeId 组的列表。 组名称与源目录中的模块名称对齐(但使用前导大写字母)。一次打印所有信息将会过多,因此可以使用另一个过滤器以组为单位打印信息。所以,再次关注点对点模块:

./waf --run "scratch/myfirst --PrintGroup=PointToPoint"TypeIds in group PointToPoint:  ns3::PointToPointChannel  ns3::PointToPointNetDevice  ns3::PointToPointRemoteChannel  ns3::PppHeader

在这里,可以用可能的 TypeId 名称来搜索属性,就像上面所示的 --PrintAttributes=ns3::PointToPointChannel 样子一样。

另一种了解属性的方法是通过 ns-3 Doxygen,有一个页面列出了模拟器中所有已注册属性。

Hooking Your Own Values

您还可以向命令行系统添加您自己的 hook。这可以相当简单的通过对命令行参数解析器使用 AddValue 方法完成。

让我们使用这个工具以完全不同的方式指定要 echo 的数据包数量。可以通过添加一个局部变量 nPacketsmain 函数来完成。我们将它初始化为 1 以匹配先前的默认行为。要允许命令行解析器更改此值,我们需要将值 hook 到解析器中。可以通过添加一个到 AddValue 的调用做到这一点。如下所示,

intmain (int argc, char *argv[]){  uint32_t nPackets = 1;  CommandLine cmd;  cmd.AddValue("nPackets", "Number of packets to echo", nPackets);  cmd.Parse (argc, argv);  ...

向下滚动脚本到我们设置 MaxPackets 属性 的地方并改变它,以便设置它为变量 nPackets 而不是常量 1。

echoClient.SetAttribute ("MaxPackets", UintegerValue (nPackets));

现在,如果你运行该脚本,并提供 --PrintHelp 参数,你会看到新的 User Argument 列表。

试一下:

$ ./waf --run "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.403s)--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.User Arguments:    --nPackets: Number of packets to echo

如果要指定需要 echo 的数据包数量,可以通过在命令行设置 --nPackets 参数做到这一点,

$ ./waf --run "scratch/myfirst --nPackets=2"

你现在应该看到

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)0s UdpEchoServerApplication:UdpEchoServer()1s UdpEchoServerApplication:StartApplication()Sent 1024 bytes to 10.1.1.22.25732s Received 1024 bytes from 10.1.1.12.25732s Echoing packetReceived 1024 bytes from 10.1.1.2Sent 1024 bytes to 10.1.1.23.25732s Received 1024 bytes from 10.1.1.13.25732s Echoing packetReceived 1024 bytes from 10.1.1.210s UdpEchoServerApplication:StopApplication()UdpEchoServerApplication:DoDispose()UdpEchoServerApplication:~UdpEchoServer()

你现在 echo 了两个数据包。很容易,不是吗?

你可以看到,如果你是 ns-3 用户可以使用命令行参数系统来控制全局值和属性。如果你是模型作者,你可以添加新的属性到你的对象,通过命令行系统他们将自动对用户可用。如果你是脚本作者,你可以相当轻松地添加新变量到你的脚本并 hook 到命令行系统。

使用追踪系统(Using the Tracing System)

仿真的目的是产生输出以便进一步研究,ns-3 追踪系统是其的主要机制。因为 ns-3 是 C++ 程序,从 C++ 程序中产生输出可以使用:

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

你甚至可以使用日志模块为你的结果添加一个小的结构。但这些会产生了许多众所周知的问题,因此我们提供了一个通用事件跟踪子系统( generic event tracing subsystem)来解决我们认为重要的问题。

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

  • 对于基本任务,追踪系统应允许用户为流行追踪源生成标准追踪,并自定义哪些对象需生成追踪;
  • 中级用户必须可以扩展追踪系统以修改生成的输出格式,或插入新的追踪源,而无需修改模拟器的核心;
  • 高级用户可以修改模拟器核心以添加新的跟踪源和宿(sink)。

ns-3 跟踪系统是建立在独立的跟踪源和跟踪宿概念上的,连接源与宿有一套统一的机制。跟踪源是可以指示发生在仿真中的事件并提供对感兴趣底层数据接入的实体。例如,跟踪源可以指示数据包何时被网络设备接收并对感兴趣的跟踪宿提供数据包内容的接入。

跟踪源对自身并没有什么用,它们必须“连接”到其他对跟踪宿有用的代码片段上。跟踪宿是跟踪源提供的事件和数据的消耗者。例如,可以创建一个跟踪宿(当连接到前一示例的跟踪源时)能够打印出接收数据包感兴趣的部分。

这种显示划分的理由是,允许用户将新类型的 sink 附加到已存在的跟踪源,而不需要编辑和重新编译模拟器核心。因此,在上面的示例中,用户可以定义新的 sink,仅通过修改用户脚本就可将其附加到定义在模拟器核心的已存在的跟踪源。

在本教程中,我们将介绍一些预定义的源和宿,并展示用户如何通过少量工作对它们进行定制。有关高级跟踪的配置(包括扩展跟踪命名空间和创建新的跟踪源)信息,请参阅 ns-3 手册或 how-to 部分。

ASCII 跟踪

ns-3 提供了助手功能,其包装了低级跟踪系统,以帮助你了解配置一些容易理解的数据包追踪的细节。如果启用此功能,你会看到输出到 ASCII 文件中。对于那些熟悉 ns-2 输出的,这种类型的跟踪类似于许多脚本生成的 out.tr

让我们投入进去,添加一些 ASCII 跟踪输出到 scratch/myfirst.cc 脚本。 在调用 Simulator::Run () 前,添加如下代码:

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

像许多其他的 ns-3 惯用语法,该代码使用 helper 对象来帮助创建 ASCII trace。第二行包含两个嵌套方法调用。“内部”的方法 CreateFileStream() 使用未命名对象惯用语法在栈上创建一个文件流(file stream)对象(无对象名)并传到被调用的方法。我们以后会多次进入这里,但此时你要知道的是你正在创建对象(其代表名为 myfirst.tr 的文件)并将其传到 ns-3。你正在告诉 ns-3 去处理创建的对象的生命周期问题,并处理一个由鲜为人知的(故意的) C++ ofstream 对象 copy 构造函数的限制所造成的问题。

外部调用 EnableAsciiAll(),告诉 helper 你想要启用模拟器中所有的点对点设备的 ASCII 追踪,并且你希望(提供的)跟踪宿以 ASCII 格式写有关数据包移动的信息。

对于那些熟悉 ns-2 的 ,跟踪事件相当于日志中的 ‘+’、’-‘、’d’和’r’ 事件。

现在可以构建脚本并从命令行运行它:

$ ./waf --run scratch/myfirst

正如你以前看到过很多次,你会看到一些来自 Waf 的消息,然后一些诸如 “‘build’ finished successfully” 之类的来自正在运行的程序的消息。

当它运行时,程序会创建一个名为 myfirst.tr 的文件。由于Waf的工作原理,该文件不会本地目录中创建,默认情况下它会在库(repository)的顶层目录中创建。如果你想控制 trace 在何地保存,你可以使用 Waf 的 --cwd 选项来指定。由于我们没有这样做,因此我们需要回到顶层目录(用你最喜欢的编辑器)来查看 ASCII 跟踪文件 myfirst.tr

Parsing Ascii Traces

这里有很多信息以非常密集的形式存在,但首先要注意的是这个文件中有许多不同种类的线。除非你放大你的窗口否则很难看清楚。

文件中的每一行对应一个跟踪事件(race event)。这次,我们正在追踪仿真中点对点网络设备传输队列(transmit queue)中的事件。传输队列是一种队列每一个目的是点对点信道的数据包必须通过。请注意,跟踪文件中的每一行都以单个字符开头(后面有空格)。 字符具有以下含义:

  • +: 发生在设备队列的入队操作;
  • -: 发生在设备队列的出队操作;
  • d: 包被丢弃,通常是因为队列已满;
  • r: 包由网络设备接收。

让我们更详细地了解跟踪文件的第一行。我将把它分为几个部分(缩进为清楚起见),在左侧有参考编号:

+2/NodeList/0/DeviceList/0/$ns3::PointToPointNetDevice/TxQueue/Enqueuens3::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)

此扩展跟踪事件(参考编号 0)的第一部分是操作。这里我们有 + 字符,所以这对应于传输队列的 入队 操作。 第二部分(参考编号 1)是以秒为单位表示的仿真时间。你可能还记得,我们要求 UdpEchoClientApplication 在 2 秒开始发送数据包。在这里我们可以确认,的确发生在 2 秒。

接下来的一部分(参考编号 2)告诉我们哪个跟踪源发起了此事件(表示在跟踪命名空间中)。你可以认为跟踪命名空间有点像文件系统(filesystem)命名空间。命名空间的根是 NodeList。这对应于以 ns-3 核心代码管理的容器,其包含一个脚本所创建的所有节点。正如一个文件系统在根目录下可能有很多目录,在 NodeList 中我们有节点编号。因此,/NodeList/0 是指 NodeList 中的零号节点,也即我们通常所认为的 node 0。在每个节点中都有已安装设备列表。此列表接下来出现在命名空间中。你可以看到本跟踪事件来自 DeviceList/0,也即安装在节点中的第零号设备。

接下来的 $ns3::PointToPointNetDevice 告诉你什么样的设备出现在节点零的设备列表的第零号位置上。回想一下,在参考编号 00 中找到操作 + 意味着在该设备上发生了入队操作。这反映在 trace path 的最后片段 TxQueue/Enqueue 上。

跟踪的其余部分应该非常直观。参考编号 3-4 表示数据包被封装在点对点协议中。参考编号 5-7 表示该数据包具有 IPv4 报头并且源 IP 地址为 10.1.1.1 发往 10.1.1.2。参考编号 8-9 表示该数据包具有 UDP 报头。最后,参考编号 10 表示有效载荷是预期的 1024 字节。

跟踪文件的下一行显示出相同的数据包在节点的输出队列上出队。

跟踪文件的第三行显示出数据包被节点上的网络设备接收。我已经在下面重现了这个事件。

r2.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)产生的。对于你而言,通过查看文件中剩余部分的 trace,现在应该很容易了解数据包通过拓扑的过程。

PCAP 跟踪

ns-3的设备助手也可以用来创建格式 .pcap 的 trace 文件。pcap 是 packet capture 的缩写(通常用小写)表示数据包捕获,它实际上是包含 .pcap 格式定义的 API。可以读取和显示此格式最流行的程序是 Wireshark(以前称为 Ethereal)。然而,有很多 traffic trace analyzer ( 流量追踪分析器 )使用该数据包格式。我们鼓励用户利用多种工具分析 pcap trace。在本教程中,我们使用 tcpdump 查看 pcap trace。

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

pointToPoint.EnablePcapAll ("myfirst");

现在,在我们刚才添加到 scratch/myfirst.cc 的 ASCII 追踪代码后插入该代码。注意,我们刚才传的字符串是 myfirst 而不是 myfirst.pcap 或者其他的什么。这是因为参数是一个前缀,而不是一个完整的文件名。helper 实际上将为仿真中的每个点对点设备创建一个跟踪文件。 文件名将基于前缀、节点编号、设备编号和 .pcap 后缀构建。

在我们的示例脚本中,我们最终看到的是名为 myfirst-0-0.pcap 和 myfirst-1-0.pcap 的文件,它们分别是 节点 0-设备 0节点 1-设备 0 的 pcap trace。

一旦添加了该代码行以启用 pcap 跟踪,就可以用通常的方式来运行脚本:

$ ./waf --run scratch/myfirst

如果你查看你的发行版的顶级目录,你现在应该看到三个日志文件:myfirst.tr ,是我们前面讨论过的 ASCII trace 文件。myfirst-0-0.pcapmyfirst-1-0.pcap 是我们刚才生成的新 pcap 文件。

使用 tcpdump 读取输出

在此时,最简单的事莫过于使用 tcpdump 查看 pcap 文件。

$ tcpdump -nn -tt -r myfirst-0-0.pcapreading 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 10242.514648 IP 10.1.1.2.9 > 10.1.1.1.49153: UDP, length 1024tcpdump -nn -tt -r myfirst-1-0.pcapreading 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 10242.257324 IP 10.1.1.2.9 > 10.1.1.1.49153: UDP, length 1024

你可以在 myfirst-0-0.pcap 的 dump 中看到 echo 数据包是在 2 秒送入仿真的。如果你看一下第二个 dump(myfirst-1-0.pcap),你可以发现该数据包在 2.257324 秒被接收。你会发现该数据包在第二个 dump,2.257324 秒被 echo。最终,你会发现在第一个 dump,2.514648 秒该数据包被接收。

使用 Wireshark 读取输出

如果你不熟悉 Wireshark,可以从以下网站下载程序和文档:
http://www.wireshark.org/

Wireshark有图形用户界面可用于显示这些跟踪文件。如果你已经有 Wireshark 了,你可以打开每个跟踪文件并显示其内容,就像你是使用一个 packet sniffer 捕获这些包一样。