本章主要介绍NS-3当中的配置路径与命令行。在NS-3当中,我们经常需要对属性进行设置,除了直接对单个已经创建的对象进行属性操作之外,我们还可以通过配置路径进行批量配置。对于追踪源也是同样的,我们除了可以对单个对象进行追踪之外,还可以考虑直接使用剖之路径进行追踪,避免要获取对象。配置路径可以和命令行参数结合使用,在启动脚本的同时传入命令行参数来进行设置,避免在改变属性设置等的时候要重新编译脚本的尴尬。
1. NS3配置路径简介
我们在配置对象的属性或者连接追踪源的时候,必须要首先获得到这个对象。然而有时候这些对象是在其他类当中创建的,我们并没有很方便的方法可以获得这些对象的指针或者引用。更糟糕的是,如果我们想对一批对象进行属性设置或者连接追踪源的时候,就必须从不同的地方获取这些对象,然后挨个进行设置或者连接,这无疑增加了编程工作的复杂程度。
实际上NS3给我们提供了一种很方便的方式来进行对象属性的设置,或者是连接追踪源。我们只要提供一个配置路径的字符串,就可以方便的对这个路径所指向的对象进行属性设置或者连接其追踪源。更巧妙的是,这种路径还可以使用通配符匹配多个对象,使得对多个对象做同样的配置变得异常的简单。
2. Config名称空间
配置路径大部分时候都和Config名称空间一起使用。Config名称空间提供了配置对象的属性和连接对象的追踪源的一些便捷函数。接下来我们看一下Config名称空间的定义:
1 | namespace Config { |
我将Config名称空间中的方法分为五类:
- 辅助方法:主要用来将用户传入的配置路径字符串转换为相应的对象或者类。
- 对象属性相关方法:主要用来对对象的属性进行设置,即对已经创建好的对象的属性进行设置。
- 类属性相关方法:对类属性的默认值进行修改,即对所有即将创建的对象的属性进行设置。
- 全局属性相关方法:这些属性不依赖于任何的对象存在,而是作为一种全局属性,类似于全局变量,在程序任何地方都可以直接使用。
- 对象追踪源的相关方法:主要用来连接或者断开和特定对象的追踪源。
- 根对象的相关方法:主要用来将一个对象设置成根对象,或者取消其根对象设置。
3. 全局属性
在NS3当中,普通的属性必须和一个对象绑定在一起,并且属性的值必须绑定到一个成员变量上。全局属性可以使我们脱离属性对象的绑定。不再需要创建任何对象(也就不需要任何成员变量),就可以使用属性。
在使用全局属性之前,我们需要创建一个全局值GlobalValue:
1 | GlobalValue uint = GlobalValue ("TestValue", "A value for test", |
从代码当中可以发现全局值的创建方式和普通属性基本相同,唯一不同的是不需要访问器,因为它不需要绑定到任何的成员变量。创建完全局值之后,我们就可以通过Config命名空间当中的SetGlobal()或者SetGlobalFailSafe()方法,对其进行设置。这两个方法的差别在于,如果调用SetGlobal(),那么属性的名称必须已经存在,否则程序就会出错;而如果调用SetGlobalFailSafe()的话,如果属性名不存在,则会返回false。
注意:NS3系统当中有很多以FailSafe结尾的函数其意义都是一样的,如果不存在这些变量,那么将返回false,而不会是程序异常终止。而很多没有FailSafe结尾的版本,在不满足条件的情况下,会使程序异常终止。具体选择哪个版本需要根据实际情况考虑来选择。
1 | GlobalValue uint = GlobalValue ("TestValue", "A value for test", |
Config名称,空间当中并没有提供任何函数来获取全局值的属性值。如果我们在设置完成之后要获取,其属性值,那么需要从GlobalValue类当中来找相应的方法。因此,我们来,看一下这个类当中有哪些主要的方法:
1 | class GlobalValue |
我们发现,GetValueByNameFailSafe()方法或者GetValueByName()方法都可以用来获取属性的值,并且这两个方法都是静态方法,不需要创建任何对象,就可以通过类名来使用。接下来我们展示完整的程序:
1 |
|
运行程序之后,可以得到如下的结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
说明我们可以通过一个简单的名字,正确的设置和获取全局属性的值。此外,Config::SetGlobal()还有一个替代方法:
1 | static void GlobalValue::Bind (std::string name, const AttributeValue &value); |
实际上,Config::SetGlobal()方法内部就是调用了GlobalValue::Bind()方法。
NS3默认在系统中创建了5个全局属性:
- SimulatorImplementationType: NS3系统使用的仿真器的实现类。默认值为ns3::DefaultSimulatorImpl.
- SchedulerType: NS3系统使用的调度器类型。默认值为ns3::MapScheduler.
- RngSeed: 所有伪随机数生成器的种子。默认值为1.
- RngRun: 所有伪随机数流的子流索引号默认值为1.
- ChecksumEnabled: 是否在协议中启用校验。默认值为false.
到目前为止只要知道这5个属性是全局属性即可,无需理解其意义,未来可以用全局属性的任何设置方式来进行设置。 全局属性的其他的设置方法,留到介绍命令行一章再进行讲解。
4. 根对象
根对象可以直接用路径“/”来访问,不管这个对象在什么位置。Config名称空间当中有四个函数和根对象相关:
1 | //将一个对象注册为根对象 |
例如接下来的例子中,我们定义了一个对象,并且在对象当中定义了一个属性:
1 |
|
随后,我们在主函数当中创建一个对象并将其注册成为根对象:
1 | Ptr<MyObject> obj = CreateObject<MyObject>(); |
随后,我们便可以通过“/”来引用该对象,当然大部分情况都是在访问其属性和追踪源时才需要用到,例如:
1 | Config::Set("/AttributeA", UintegerValue(50)); |
完整程序如下:
1 |
|
运行程序得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见设置为根对象的对象,其属性可以通过“/”路径加属性名字直接设置。
如果有两个对象都被设置为根对象,但是他们有同样的属性,会发生什么事情呢?下面我们通过一个例子来说明:
1 |
|
程序中我们创建了两个类MyObject和MyObject2,他们都具有一个同名的属性AttributeA。在主函数当中,我们创建了这两个类的实例obj和obj2,然后将他们都注册为根对象。随后我们通过Config::Set()函数修改了根对象的AttributeA属性,运行程序之后,我们得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见此时两个对象的AttributeA属性都被修改。这个例子告诉我们一个道理,使用路径进行属性配置的时候,可以一次对多个属性进行设置。接下来我们通过一个例子来看一下,如果将一个对象注册两次根对象,会有什么情况?
1 |
|
程序中我们创建了MyObject类的一个实例obj,然后三次将该对象注册为根对象。最后输出系统中根对象的数量。运行程序之后,我们得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可以看出,重复注册同一个对象之后,系统中根对象的数量会是实际注册的次数。换句话说,在NS3当中,对象是可以被重复注册为根对象的。当然这对我们,对根对象的属性进行设置,并没有太大的影响。因为对同一个值设置三次,其结果还是一样的,只是在性能上会有所损失。
5. 名称对象
除了将对象注册到根对象之外,NS3还提供了一个称为名称对象(Names)的类。从名称对象当中,可以將任何的NS3对象绑定到一个名称上。类似于编程当中的键值对类map,使用一个字符串来索引一个对象。通过Names类添加的方法和根对象最大的区别在于,其所有对象都会出现在“/Names/”配置路径下,而注册为根对象的对象的路径为“/”。Names类提供了一些实用的方法:
1 | class Names |
这些方法主要分为四类:
- 添加:将一个对象和一个名称绑定
- 改名:修改一个对象所绑定的名称
- 清空:清除所有对象的名称绑定
- 查找:通过对象查找名称或者通过名称查找对象
5.1. 添加
添加方法一共有三种重载。他们的作用都是将一个对象注册到配置路径“/Names/”及其子路径下。第一种重载最为实用,只需要提供路径名称和要注册的对象即可。我们通过下面的例子来解释:
1 |
|
程序将一个MyObject对象绑定到了一个配置路径“/Names/object”上,然后试图获取其对象的绑定路径。运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,绑定之后,可以通过FindPath()方法来获取对象所对应的路径。在绑定对象时,不指定完全路径结果是一致的,无论如何通过Names::Add()绑定的对象路径一定是在“/Names/”路径下。例如,下面的两句代码,其结果是一样的:
1 | …… |
和根对象不同,通过Names::Add()是不能重复注册对象的。例如下面代码,试图将一个对象注册两次,分别注册到不同的配置路径当中:
1 | int |
运行程序将得到如下的错误:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
证明在添加第二个路径object2的时候出现了错误。Names也不支持将两个不同的对象注册到同一个路径下。例如下面的代码:
1 | int |
试图将两个不同的对象obj1和obj2都注册到同一个路径“/Names/object”下面。运行程序之后将得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
当然,这种情况下,我们不知道究竟是错在第一个Add()方法还是错在第二个。因此我们输入如下命令进入gdb调试模式(参考运行环境配置一章的相关部分):
1 | ./waf --run "try-names-path" --command-template="gdb %s" |
在进入gdb调试模式之后,我们可以使用run命令来启动程序,等程序出错之后,可以使用backtrace程序查看调用情况:
1 | (gdb) r |
从backtrace的信息当中可以看出,代码主要出错在我们程序的第71行,错误位置主要是在主函数当中。因此,我们可以输入如下命令来列出程序内容,从而确定错误在什么地方:
1 | (gdb) list ../scratch/try-names-path.cc:65,71 |
其中文件名路径为“../scratch/try-names-path.cc”的原因在于NS3运行的工作路径路径是在build目录当中,因此必须往上一层才能找到NS3的主目录。
或者是直接指定列出主函数main为标志的函数内容:
1 | (gdb) list main:65,71 |
从中可以看到,第71行是第二次添加对象的时候。
Names添加对象的时候,也可以使用层次路径,并且这种层次路径可以让本来不存在层次关系的对象产生层次关系。例如下面的代码当中:
1 | int |
创建了两个对象obj2和obj,我们先将obj2注册到路径“/Names/object”当中,再将obj对象注册到“/Names/object/obj”路径当中。运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
在注册层次路径的时候需要注意的是,要注册子路径,上一层路径必须已经存在。否则就如下面的程序一样:
1 | int |
程序中和上一个程序唯一的区别就是调换了注册对象的顺序,先注册了子路径,再注册父路径。运行程序之后得到如下的结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
从运行结果可以看出,如果在注册子路径的时候,父路径还不存在,那么这一层路径将被忽略。这一点在使用的时候要特别引起注意。
第二个Add()方法的重载主要是在添加子路径的时候使用的,其三个参数分别表示:父路径、子路径的名字和需要注册的对象。例如下面的代码完成的功能和上面的代码完全一致:
1 | int |
只不过程序当中使用了Add()方法的第二种重载而已。运行程序之后得到如下的结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,使用这种方式也可以添加层次路径。然而这种方法和第一种方法相比并没有太大的优势。除非在需要重复添加多个子路径的时候,可能可以避免拼接字符串。和第一个重载一样,需要注意的是,在注册子路径的时候,父路径必须已经存在。
Add()方法的第三个重载也主要是用于添加子路径,但是在有些情况下,我们只知道主路径的对象时谁,而不知道主路径是什么,这时我们可以可以使用第三个重载。其三个参数分别表示:父对象、子路径的名字和需要注册的对象。例如下面的代码完成的功能和上面的代码是完全一致的:
1 | int |
只是我们在第二个Add()方法当中传入了父对象,而不是一个路径。运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
使用这个重载方法时需要注意的是,在添加子路径的时候,主对象必须已经被绑定到一个路径上。
5.2. 改名
用Add()方法注册了对象的路径之后,还可以使用Rename()方法来进行修改。和Add()方法类似的,Rename()方法一共有三种重载。使用的时候需要注意的是,Rename()方法只能修改,本对象的路径而不能修改其父路径。
下面我们通过一个例子来说明Rename()方法的使用:
1 | int |
运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
Rename()方法的第二个参数,不能是路径,只能是名字,否则会出现奇怪的现象,例如下面的程序:
1 | int |
在修改名字的时候,传入了一个路径help/object1。运行程序后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
很显然,程序当中是不存在/Names/help对象的,但是却可以改名到这个路径下。还记得前面添加路径的时候是无法添加到空路径当中的。此外,如果传入更奇怪的值,改名的行为就更加奇怪了。例如下面的程序:
1 | int |
其中在改名路径时传入了一个非常奇怪的值“/Names/Names/object1”,运行程序后将得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
再来个更奇怪的:
1 | int |
运行之后得到结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
这次更加奇怪,连路径的名字都没有了!很奇怪吧!这种情况我们一般认为程序员又偷懒了,在改名的时候没有检查路径是否真实存在,只是简单地将两个字符串相连接而已。虽然在使用上并不会出现什么问题(?),但是我们还是将这认为是NS3(3.29)的bug。使用的时候要非常小心。
当然,改名的时候还是要考虑是不能重名的,例如:
1 | int |
运行之后会得到错误:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
其次,对于本来就是层次路径的情况,如果修改了父路径的名字,那么子路径将是怎么样的呢?我们用下面的程序来进行测试:
1 | int |
运行程序得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见当父路径更新之后,其下所有引用该路径的子路径也会相应进行修改。
Rename()方法的重载二和重载三的情况和Add()方法非常相似,直接查看API就能明白,就不再赘述。
5.3. 查找
查找主要分为两个方向:
- 通过对象查找路径
names.cc 1
2
3
4
5class Names
{
static std::string FindName (Ptr<Object> object);
static std::string FindPath (Ptr<Object> object);
}; - 通过路径查找对象
names.cc 1
2
3
4
5
6
7
8
9
10
11class Names
{
template <typename T>
static Ptr<T> Find (std::string path);
template <typename T>
static Ptr<T> Find (std::string path, std::string name);
template <typename T>
static Ptr<T> Find (Ptr<Object> context, std::string name);
};
通过对象查找路径,在前面的一些例子当中已经演示过了。只不过主要演示的是FindPath()方法,这里用一个例子来演示FindName方法。
1 | int |
运行之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见FindPath()得到的是整个路径,而FindName()得到只是对象所对应的最后一个名称。
其次是通过路径查找对象。而这种查找也主要有三个重载,其形式和Add()方法一致,这里就主要以第一个重载进行演示。
1 | int |
运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
6. 配置路径控制属性和追踪源
6.1. 配置路径
配置路径的概念非常的简单,就是使用一种特殊的字符串语法,从根对象(“/”)开始,引用到某个属性或者追踪源。其实我们在上面的程序设置根对象的属性的时候,已经使用过配置路径“/AttributeA”。当然,这是一个比较简单的例子。配置路径所能够引用的对象是非常复杂的。下面我们依次讲解这些复杂的配置路径语法。
6.1.1. 层次路径
如果一个对象的属性是另外一个对象,那么就可以用层次路径来访问。例如下面的例子当中:
1 | class MyObject : public Object |
MyObject2类有一个属性object,其类型是MyObject的对象,而MyObject类则有一个属性AttributeA,是一个普通uint32_t属性。我们将MyObject2的对象实例注册为根对象,因此,可以使用配置路径“/object”来访问MyObject类型的属性。一旦“/object”被设置之后,即可通过“/object/AttributeA”来访问到MyObject其中的uint32_t属性。例如主函数为:
1 | int |
程序中创建了一个MyObject2的对象实例obj2,然后将其设置为根对象。然后创建了一个MyObject类型的对象实例obj,然后使用配置路径“/object”,将obj设置到obj2当中。随后我们便可以通过配置路径“/object/AttributeA”来设置obj对象的属性。程序运行之后,我们得到结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,只要一个对象的某个属性是另外一个对象(其属性值类型是PointerValue),那么即可通过层次路径的方式来访问另外一个对象的任何属性。如果另外一个对象的属性又是第三个对象,那么层次路径是可以嵌套使用的,例如:“/object1/object2/attribute1”。
完整的程序如下:
1 |
|
6.1.2. 层次路径的通配符
如果在一个对象的属性当中,有多个属性都是对象属性(PointerValue),并且恰巧这些对象属性都有同样的属性名称,那么即可通过通配符来同时访问这些属性。例如下面的例子:
1 | class MyObject : public Object |
其中定义了三个类:MyObject、MyAnotherObject以及MyObject2。MyObject2当中具有两个属性(object1和object2)其类型都为MyObject;还有另外一个属性objectA,其类型为MyAnotherObject。巧合的是,MyObject和MyAnotherObject当中都有一个属性名为AttributeA。接下来我们将在主函数当中使用通配符的形式访问这三个属性:
1 | int |
我们将MyObject2的对象设置为根对象,然后在其中设置上其object1、object2和objectA三个属性,并指向三个不同的对象。随后,我们便可以通过配置路径“//AttributeA”来同时访问三个对象当中的AttributeA属性。其中通配符“”表示匹配任何具有AttributeA属性的对象属性。
完整的程序如下:
1 |
|
运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,可以通过通配符的形式,给多个对象的同一属性进行赋值。
要是多个对象具有同名的属性,但是属性值类型不同会有什么样的结果呢?例如,我们将上面例子当中的MyAnotherObject对象的AttributeA属性的值类型换成DoubleValue:
1 | class MyAnotherObject : public Object |
然后再次运行程序,得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,多个不同的对象当中如果有名称相同但是类型不同的属性,那么在使用带有通配符的配置路径进行设置的时候要非常小心,因为不同的类型设置可能会引起程序错误。当然对于上面的这个例子,会有一些取巧的情况使得程序不会出错,例如下面的代码当中:
1 | int |
MyAnotherObject当中的AttributeA属性值类型依旧为DoubleValue。然而,在NS3当中,几乎所有的属性值类型都和字符串属性值类型兼容(请参看属性框架一章),因此,程序在使用带有通配符的配置路径的时候,如果使用了字符值属性类型,那么有可能会在字符类型和实际类型之间自动转换成功。例如StringValue(“100”)可以自动转换为UintegerValue(100)和DoubleValue(100.0)。我们运行程序之后将得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,换成字符串值类型之后,所有对象的属性都能够设置成功。但是使用的时候需要非常的小心,因为这有可能不是我们期望的结果。
6.1.3. 访问列表对象
在NS3当中,对象的属性也可以是列表类型(属性值类型为ObjectVectorValue,参见属性框架一章),由多个对象组成。配置路径可以使用特殊的语法来访问当中的任何对象。例如在下面的例子当中:
1 |
|
程序当中创建了一个MyObject类(其中具有一个AttributeA属性是整型类型)和一个MyObject2类(一种具有一个objects属性,其类型是一个关于MyObject类的列表,vector类型)。在主函数当中,我们创建了一个MyObject2类的实例,并将其设置为根对象。随后在程序中创建了四个类MyObject的实例,并将这四个实力放到根对象当中。换句话说现在根对象的objects的属性,具有四个对象。随后我们通过配置路径难设置这四个对象当中的一些或者全部。
6.1.3.1. 访问单个对象
今天我们看看如何用配置路径访问列表中的单个对象:
1 | int |
运行程序之后可以得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,如果属性值是一个列表对象,那么我们可以使用属性,名后面加数字的方式来设置列表中的某一个对象,就像在使用数组一样。并且,其第一个元素的索引也是从0开始计算的。
6.1.3.2. 访问连续的多个对象
在配置路径当中,我们可以使用语法“[x-y]”来访问索引从x到y的多个连续的对象,例如:
1 | int |
运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
6.1.3.3. 访问不连续的多个对象
在配置路径当中,可以使用语法“x|y”访问来访问索引为x和y的对象。例如:
1 | int |
运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
6.1.3.4. 综合访问连续和不连续的对象
配置路径当中,“[x-y]”和“a|b”语法可以综合使用。例如:
1 | int |
运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
6.1.3.5. 访问所有对象
可以使用通配符“*”来访问列表中所有的对象:
1 | int |
毫无意外的,其运行结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
6.1.4. 访问聚合对象
配置路径除了可以访问属性之外,还可以用于访问一个对象当中聚合的对象。由于一个对象当中聚合的所有其他对象中同类型的只能有一个(请参考对象聚合一章),因此我们可以使用对象的类型来访问聚合对象。因此NS3提供了一种特殊的语法,来通过对象类型访问一个对象当中聚合的其他对象。和访问普通的属性类似的,只要在配置路径上写符号“$”并在其后加上要访问的聚合对象的类的全名(也就是在TypeId当中注册的类的名字)即可访问该对象。例如在下面的例子当中:
1 |
|
代码创建了三个类MyObject、MyAnotherObject和MyObject2,MyObject类当中有一个属性AttributeA(类型是整型),而MyObject2类当中有一个属性object1(类型是MyObject的指针)。主程序当中,我们创建了一个MyObject2的实例对象root,并将其注册为根对象。然后和前面的例子一样,我们使用配置路径将,MyObject类的实例对象设置到root当中。除此之外我们将MyAnotherObject类的实例对象objA聚合到root当中。随后我们便可以通过配置路径访问root的属性对象和聚合对象:
1 | //配置普通属性 |
普通属性的访问方式和之前介绍的一样。而聚合对象的访问方式,则可以通过对象类型来直接引用。完整的程序如下:
1 |
|
运行程序之后,得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
6.2. 使用配置路径
配置路径主要是用来在设置对象的属性或者连接其追踪源的时候,不需要获得对象的实例,而是可以通过一个简单的字符串就直接获取该属性或者追踪源。
在NS3当中,配置路径总是从根“/”开始的,所有被注册成为根对象的对象都可以直接通过“/AttributeName”来直接访问其名为“AttributeName”的属性。这个在前文已有描述,在此不再赘述。
除此之外,NS3当中有两个内置的路径:“/Names/”和“/NodeList/”。
所有通过Names::Add()方法注册的对象都会出现在“/Names/”路径下。这个在前文也有描述。这里通过一个简单的例子演示根对象和Names对象的属性设置:
1 |
|
程序演示了如何将对象注册为根对象,并添加子对象,并使用Config::Set()方法通过配置路径来进行属性设置。此外,还通过Names::Add()方法将对象注册到“/Names/”路径下,并使用Config::Set()方法通过配置路径来进行属性设置。仔细看的话会发现,程序中注册到根对象“/object/”中的子对象obj和注册到“/Names/”路径下的obj对象是同一个。这说明,可以通过多种不同的方式,来将一个对象注册到不算的配置路径下,配置任何一个路径,都可以修改属性值。运行程序后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
对于“/NodeList/”路径,每次在NS3程序当中创建一个Node(这是NS3网络概念中的基础类,稍后的章节会有介绍),这个Node实例都会自动被添加到“/NodeList/”路径下(实际上NodeList是NodeListPriv类的一个属性,在创建第一个节点的时候,NodeListPriv被设置为了根对象,因此可以使用“/NodeList”来访问这个属性,当然这个细节无需知道,只需要知道有这么个路径可以访问所有Node实例即可),并且可以通过Node的Id(一个自动的整型计数器,自动记录了当前创建的Node对象是第几个,计数从0开始)子路径来访问。
下面通过一个例子来说明“/NodeList/”的创建和使用方法:
1 |
|
在这个例子当中,我创建了一个节点node1,并将obj2对象聚合到其中,最后使用配置路径的形式将对象obj设置到obj2的属性上。然后创建了一个节点node2,然将对象的objA聚合到其中。随后便可以通过配置路径对obj对象和objA对象的AttributeA属性进行配置。因为node1是第一个创建的节点,因此其索引编号为0,而node2是第二个创建的节点,其索引编号为1。因此可以使用路径“/NodeList/0”引用node1,而使用“/NodeList/1”引用node2。其运行结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
配置路径除了可以设置属性之外,还能被用于将函数或方法添加到追踪源。下面通过一个例子来说明:
1 |
|
程序当中创建了三个类:MyObject,MyObject2以及MyAnotherObject。这三个类中分别定义了追踪源依次为:数值追踪源、事件追踪源和事件追踪源。随后主程序中分别创建了三个类的实例依次为:obj,obj2以及objA。obj对象被注册为根对象,而obj2对象被添加到Names路径中,最后,objA对象被聚合到一个节点当中。程序使用Config名称空间当中的函数将这些追踪源关联到各自的函数。最后触发各种事件。其中需要注意的是连接追踪源的两种函数ConnectWithoutContext()和Connect()。第一种方法是正常方法,根据用户定义的追踪源的TracedCallback的参数确定关联函数的参数;第二种方法比较特殊,它会将关联的路径作为第一个参数传给最终的关联函数,因此定义函数的时候必须比TracedCallback当中定义的参数多一个字符串类型作为第一个参数。运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
由此可见,无论哪种方式创建的路径,都可以通过Config名称空间的方法ConnectWithoutContext()或者Connect()正确地关联追踪源。