本章主要介绍NS-3当中的命令行。在NS-3当中,我们经常需要对属性进行设置,除了直接对单个已经创建的对象进行属性操作之外,我们还可以通过配置路径进行批量配置。配置路径可以和命令行参数结合使用,在启动脚本的同时传入命令行参数来进行设置,避免在改变属性设置等的时候要重新编译脚本的尴尬。除此之外,也可以使用命令行来改变程序当中变量的值,也可以避免因修改代码而重新编译脚本。
1. NS3命令行简介
在一个正常的仿真脚本当中,需要对各种情况进行仿真,因此需要不断的改变属性的值,来创建不同的仿真场景。然而改变了属性的值之后,整个仿真脚本就得重新编译,比较繁琐。NS3提供了一种机制,可以使用命令行的参数来改变属性的值,此时就无需重新编译脚本来仿真各种场景。这种机制主要通过命令行类来实验。除了控制已有的NS3对象属性之外,命令行也可以通过参数来动态的改变变量的值。使得我们控制各种变量的时候无需重新编译脚本。
2. 使用命令行类解析命令行参数
要使用命令行类比较简单,只要在脚本当中创建命令行类,并且调用其解析方法,就可以解析命令行参数。例如下面的例子当中:
1 |
|
只能创建命令行类,然后将程序的命令行参数传给解析方法即可。此时我们的程序已经可以解析命令行参数。NS3命令行类内置了一些参数,我们可以使用–PrintHelp参数来查看。例如我们输入如下命令运行上面的程序,并添加–PrintHelp参数:
1 | $ ./waf --run "try-command-line --PrintHelp" |
需要注意的是,如果程序带有命令行参数,那么程序名和传递给程序的命令行参数必须有双引号引起。程序运行后可以得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
从程序的运行结果可以看出,NS3命令行类默认可以解析,一些命令行参数,除了“–PrintHelp”之外,还有:
- –PrintGlobals:打印全局变属性
- –PrintGroups:打印所有的组
- –PrintGroup=[group]:打印一个组当中所有的TypeId
- –PrintTypeIds:打印所有的TypeId
- –PrintAttributes=[typeid]:打印一个TypeId当中所有的属性
- –PrintHelp:打印命令行帮助
如果有兴趣这些命令都请自行尝试,我们仅仅以PrintGroups、PrintGroup即PrintAttributes为例讲解命令行参数的使用。
首先,我们可以使用–PrintGroups打印出系统中所有的组(组是在创建TypeId的时候通过SetGroupName()方法设置的,目的是将不同的类分到不同的组当中,仅仅为了方便组织,无实际意义):
1 | $ ./waf –run "try-command-line –PrintGroups" |
随后可以使用–PrintGroup=group来查看组内的类的详情,例如我们查看Internet这个组当中所有的类的TypeId:
1 | $ ./waf –run "try-command-line –PrintGroup=Internet" |
可见,所有属于Internet这个组的类全都被列了出来。如果我们对某个类比较感兴趣,可以进一步使用–PrintAttributes=typeid将其详细的属性信息列举出来。例如,我们对TcpSocketBase这个类比较感兴趣,想看看其中有什么属性,可以使用如下命令:
1 | $ ./waf --run "try-command-line --PrintAttributes=ns3::TcpSocketBase" |
可见,程序列出了所有TcpSocketBase类支持的属性。每一项属性都展示了其名称、默认值和帮助信息。
需要注意的是,这里列出的属性信息和我们通过API文档查询到的属性信息是一致的。(那还不如去查文档?)但是,自己创建的类、引用了第三方的类或者修改过NS3系统自带的类,自行添加的属性文档上是不可能会有的,只能通过这种方式查询。
3. 使用命令行设置属性的默认值
命令行参数可以覆盖属性的默认值,只需要在命令行参数当中指出具体的类的全名(TypeId中设置的名称)和需要覆盖的属性,然后再对其赋值。程序中就可以不再对属性设置任何的值,而所有以该类创建的所有的对象都将具有设置的默认值。
例如下面的例子当中:
1 |
|
定义了一个对象MyObject,其中定义了一个属性AttributeA。主函数当中,程序解析了命令行参数,随后创建了一个MyObject类的实例对象obj,并输出了其AttributeA属性背后的成员变量的值。如果我们运行程序的时候,不输入命令行参数,那么属性值将是默认值:
1 | ./waf --run "try-command-line-attribute"Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
然而,我们再不用修改和重新编译程序的情况下,只需要指定命令行参数,即可修改属性的默认值。例如我们使用如下的命令运行程序:
1 | $ ./waf --run "try-command-line-attribute --ns3::MyObject::AttributeA=199" |
这可以巧妙地解决每次修改程序的配置都必须重新编译程序的问题。然而这只适用与所有对象都有同样属性值的情况。要修改某一个实例的属性值用这种方式是不行的,只能通过Config::Set()方法使用配置路径修改。
然而这种方式最大的问题在于,每次需要设置属性的时候,都需要写属性的完全的配置路径(例如ns3::MyObject::AttributeA=199),相对来说比较繁琐。因此NS3提供了一种相对简化的写法,可以将较长的配置路径映射成一个较短的名称。只要调用CommandLine实例对象的AddValue()方法:
1 | void AddValue(const std::string & name, const std::string & attributePath); |
你可将一个较长的属性的配置路径attributePath映射成为一个较短的名字name。例如下面的例子:
1 | int |
我们将较长的属性配置路径“ns3::MyObject::AttributeA”映射成了一个较短的名字“attrA”。在传递命令行参数的时候,只要使用较短的名字就可以替代比较复杂的属性路径。因此,我们可以使用如下的命令运行程序,达到和之前同样的效果:
1 | $ ./waf --run "try-command-line-attribute --attrA=199" |
当然做了重新映射之后,原来的属性配置路径依然有效。使用这种映射的时候需要注意的是AddValue()方法必须在Parse()方法之前调用才有效。
除了会当中的属性之外,命令行参数也可以设置全局属性的默认值。其设置方法和类的属性默认值一致。例如,下面的例子:
1 |
|
程序中创建了一个全局属性GlobalAttribute,然后解析命令行参数,如果命令行参数当中指定了全局属性,那么全局属性的默认值将被覆盖。使用如下命令覆盖全剧属性的值:
1 | $ ./waf --run "try-command-line-global-attribute --GlobalAttribute=188" |
4. 使用命令行设置变量的值
除了可以使用命令行参数来设置类的属性默认值之外,还可以使用命令行参数来改变全局变量或者局部变量的默认值。在设置变量的默认值之前必须通过AddValue()方法将一个名称和变量值进行绑定。
1 | template <typename T> |
其第一个参数表示命令行参数的名字,第二个参数表示对参数的解释(帮助文档),第三个参数是需要绑定的变量。然后在命令行当中对参数名字设置值即可。
例如如下程序:
1 |
|
其中定义了一个全局变量和一个局部变量,然后将两个变量都绑定到命令行参数当中。如果我们在运行程序时不在命令行指定任何参数,那么程序将直接使用默认值:
1 | $ ./waf --run "try-command-line-variable" |
当然,我们也可以在不修改和重新编译程序的情况下改变一个或者两个变量的值:
1 | $ ./waf --run "try-command-line-variable --total=100 --local=189" |
使用命令行参数设置数值类型变量的时候,直接设置其数值即可。然而在使用命令行参数设置布尔型变量的值的时候有几个特殊的值可以使用。
- 表示为真:1、t、true
- 表示为假:0、f、false
- 表示取非(t变f、f变t):只有参数名,不写数值
我们使用下面的例子来演示布尔类型的变量的命令行参数设置方法:
1 |
|
和上面的程序非常类似,我们定义了一个全局变量和几个局部变量。不同的是,这次我们使用的是bool类型。我们使用以下命令启动程序:
1 | $ ./waf --run "try-command-line-bool-variable --total=f --local=0 --local=true --toggle1=0 --toggle1 --toggle2 --toggle2 --toggle2" |
我们来解析一下命令行参数和输出:
- 对于total变量,程序中初始值为true,在命令行中赋值为f(代表false),因此最终输出false。
- 对于local变量,程序中初始值为false,在命令行中赋值两次,第一次为0(代表false),第二次为true,第二次赋值将覆盖第一次的,因此最终输出结果为true。对于连续赋值,只要注意最后一次赋值的结果即可。
- 对于toggle1变量,程序中初始值为true,在命令行中赋值一次,切换一次。第一次赋值为0(代表false),切换的时候将取非,因此,最终输出true。
- 对于toggle2变量,程序中初始值为true,在命令行中切换三次。切换奇数次的结果和切换一次是一样的,切换偶数次和没有切换是一样的。因此,最终输出结果为false。
经验:由于命令行参数可以对变量重复赋值,特别是布尔类型的变量,可以重复切换,很难从命令行当中看出各个参数最后的值是什么样的,因此最佳的实践就是在程序当中将最终的取值打印出来,以便确认后再开始仿真。
5. 使用命令行通过回调设置变量
有些时候,当我们通过命令行参数设置变量的时候,会遇到一种情况就是这个变量的值是依赖于一个逻辑的,而不是直接使用某一个值。比如用户输入的值需要进行一定的换算才能在程序当中直接使用。因此,我们需要运行一段代码来完成变量赋值的逻辑。在这种情况下可以通过回调来执行这种赋值逻辑。我们通过下面的例子来解释命令行回调的作用:
1 |
|
程序中定义了一个命令行参数temp,表示温度。我们比较习惯使用摄氏度,因此需要用户输入摄氏度,然而假设在程序中,由于某种原因,我们只能内部使用华氏度,因此在设置内部变量的值的时候,我们必须做一次转换。因此我们写了一个函数ConvertTemp()。由于命令行参数必须要求回调的返回值为bool,而参数必须为std::string。因此,我们实现的ConvertTemp()函数必须满足这种要求。但是温度是一个数值,因此,我们利用了NS3的属性值可以直接从字符串转换的特性,直接将输入的字符串转换成一个浮点数,然后再参与转换。最终我们通过如下命令来运行程序:
1 | $ ./waf --run "try-command-line-callback --temp=10" |
可见,我们参数传入的值是10,而最后设置到变量上的值是50。
6. 非选项命令行参数
传统的命令行参数必须使用“–name=value”的形式调用。有时候,我们只想去输入数值,而不去输入参数的名字。这个时候可以使用非选项参数。例如下面的程序:
1 |
|
其中我们定义了一个非选项参数local,并将其绑定到一个局部变量local上。我们在设置参数local的值的时候只需要使用如下的命令行参数:
1 | ./waf --run "try-command-line-nonoption 10" |
如果任何参数都不提供,那么变量将会是默认值:
1 | $ ./waf --run "try-command-line-nonoption" |