本章主要介绍NS-3的对象框架当中的属性框架。ObjectBase类实现了属性框架的基本功能,任何类只要继承自ObjectBase即可以使用NS-3的属性框架。使用NS-3的属性框架,可以方便地对对象的属性进行设置和读取。此外,可以设置类的默认属性,设置完成后,所有该类的实例对象都具有该默认属性。在NS-3中,所有内置属性的值都能和字符串属性类型相互转换。因此,任何属性的值都可以方便地存入文件,或者从文件读取。
1. 属性框架简介
面向对象系统中,类和对象是处于核心地位的元素。对象和对象之间的差别,主要靠成员变量来体现差异。而面向对象的封装特性要求我们在实现系统时,将成员变量尽量隐藏。在传统的编程任务中,我们通常将成员变量的访问通过使用Getter和Setter方法来进行,这样做的好处是,可以将成员变量的控制逻辑一同封装在类当中。
NS-3在传统成员变量的基础上,提出了属性框架的概念。使用属性框架可以实现比传统成员变量封装更多的功能,例如:为所有对象设置一个属性的默认值,将类的属性的默认值、或者对象的值存储到文件中,然后再从文件中读取。使用追踪系统追踪某个属性的值的变化情况等等。
NS-3的属性框架主要是通过ObjectBase和TypeId来实现的,因此要使用属性框架的类,必须继承自ObjectBase,并且维护一个TypeId实例。
1.1. TypeId中关于属性框架的方法
上一章中,我们解析了ObjectBase类和TypeId类的公有方法(请查看上一章)。在介绍TypeId时,我们省略了和属性系统相关的一些方法:
1 | …… |
其中最值得注意的就是AddAttribute()方法该方法有两个重载(它们的差别在于有无flags参数)。在上一章中,我们介绍过TypeId用来表示一个类的信息。因此,AddAttribute()方法,用于向TypeId所表示的类中添加一个新的属性。添加以后,我们使用该类的时候就可以使用该属性了。我们解析以下AddAttribute()方法的参数的意义:
- name: 属性的名字
- help: 用一句话来解释一下属性的作用
- flags: 用于控制属性读写特性的参数
- initialValue: 属性的初始值,类型为AttributeValue。AttributeValue是一切属性值类型的父类,例如整形值UintegerValue,浮点数值DoubleValue以及字符串值StringValue等。本章后面部分会介绍不同的属性值类型。
- accessor: 如何访问该属性,是通过对象的成员变量访问呢?还是通过Getter/Setter方法来访问?其类型是一个AttributeAccessor的子类。
- checker: 检查属性的值是否符合要求
- supportLevel: 表示这个属性是处于使用状态、不推荐状态,还是过时状态,其类型是一个枚举变量,共有三种值:
- SUPPORT: 属性正在受到支持,可以正常使用,默认值
- DEPRECATED:属性快要过时,不支持使用
- OBSOLETE:属性已经过时,不能使用
- supportMsg: 支持性字符串,当supportLeve为不同的值时,msg的值也将不同:
- SUPPORT: 无作用,可以使用默认值空字符串(“”)
- DEPRECATED: 提示属性快要过时,给出解决方法,以后该用什么属性替代该属性
- OBSOLETE: 提示属性已经过时,不能使用,并提示使用什么属性来替代
其中flags参数的取值是由TypeId中定义的枚举类型描述的:
1 | enum AttributeFlag { |
在没有flags参数的重载中,flags的值默认为ATTR_SGC,即包含全部的特性。
1.2. 定义一个属性
任何一个NS-3属性的背后,都有一个传统的成员变量的用于存储属性的值。因此必须先在类当中定义一个合适的变量。然而在此之前,为了使用属性框架,类必须继承自ObjectBase,并提供GetTypeId()方法。当然为了使用智能指针,类还必须同时继承自SimpleRefCount(参考上一章的例子)。实际上,NS-3提供了另外一个类Object,实际上就是继承自SimpleRefCount和ObjectBase。Object的的定义如下:
1 | class Object : public SimpleRefCount<Object, ObjectBase, ObjectDeleter> |
可见,Object实际上就是继承了SimpleRefCount和ObjectBase。除此之外,Object类还实现了GetInstanceTypeId()方法,而子类无需在重写此方法。然而要注意,要让GetInstanceTypeId()自动能够获取任意子类的正确TypeId类型,必须使用NS-3提供的CreateObject()函数来创建对象,否则GetInstanceTypeId()方法返回的总是Object自己的TypeId。而NS-3的整个属性框架的基础就是TypeId,因此必须保证TypeId能够描述正确的类。
因此,我们的类要使用智能指针和属性框架,只需要继承自Object类即可。
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
上面的例子当中定义了一个类MyObject,该类继承了SimpleRefCount类,所以具有智能指针的特性。此外,SimpleRefCount的模板中还指定了其父类为ObjectBase,因此该类必然拥有TypeId(通过重写GetTypeId()获得),并且可以使用NS-3的属性系统。其TypeId定义了类的名字、父类的TypeId、所属的组以及构造函数。此外,我们还定义了一个私有变量m_myValue(其中m_是一个C++编码规范,说明其是一个成员变量),其类型为32位无符号整型(uint32_t)。
然后,我们使用AddAttribute()方法向该类的TypeId中,添加属性的描述:
1 | …… |
这段代码向MyObject的TypeId当中添加了一个属性,这个属性有如下的特征:
- 属性的名字叫MyValue
- 属性具有可读、可写两种特性
- 属性的初始值为100
- 属性被绑定到成员变量m_myValue上
- 属性必须满足类型是32位无符号整型,并且无(自定义的)取值范围限制
此外NS-3还提供了一个CreateObject
如果仔细观察可以发现,MyObject类的构造函数上,也对m_myValue变量进行了初始化,并且初始值为0。而此处属性框架又对该变量进行了初始化。我们运行下面的程序来查看到底哪个初始化在起作用。
1 | int |
最后得到的结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
从输出的结果可见,属性在创建对象的过程中已经被初始化,并且初始化当中属性中的初始值覆盖了构造函数当中的初始值。其原因在于,CreateObject()函数中,创建对象时构造函数初始化成员变量之后又CreateObject()函数又调用了初始化属性的函数CompleteConsruct():
1 | template <typename T> |
然而,属性要能够初始化成功,还有一个必要条件,那就是属性的flags参数必须为ATTR_CONSTRUCT或者ATTR_SGC二者之一,换句话说,必须包含ATTR_CONSTRUCT。 下面我们将属性改成这两个值之外的值:
1 | …… |
然后再次运行程序,可以得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可以看出,属性并没有别初始化为100,原因在于,我们并没有允许属性在构造时初始化,因此并不会给成员变量赋值,成员变量的值仍然为构造函数所初始化的值0。
1.3. 设置和访问属性
除了调用Getter和Setter方法来访问和设置属性之外,ObjectBase还提供了GetAttribute和SetAttribute两个方法来完成同样的工作,即便是在我们自己没有实现Getter/Setter方法的情况下,这两个方法一直都存在。
例如,下面的例子通过SetAttribute()方法来设置属性的值,并通过GetAttribute()方法来读取属性的值:
1 | int |
运行结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
也可以使用CreateObjectWithAttributes()函数在创建对象的时候就设置属性,其语法和CreateObject()函数类似,但是可以添加属性的名称和值作为参数:
1 | Ptr<Object> obj = CreateObjectWithAttributes("name1", value1, "name2", value2, ..., "name9", value9); |
如上面的语法所示,这个函数可以在创建对象的时候最多设置设置9个属性(也可以完全不设置)。需要注意的是使用这种方法初始化属性,属性必须具有构造属性(TypeId::ATTR_CONSTRUCT)。例如,对于上面的例子,我们也可以在创建对象的时候就初始化其属性值:
1 | …… |
其运行结果和上面程序一致:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
1.4. 改变属性默认值
在向TypeId当中添加属性的时候,我们也默认设置了属性的默认值。然而,很多时候,我们要创建一批对象,这些对象拥有同样的属性值,然而,这些属性值又和默认值不一致。NS-3提供了一种比创建一批对象,然后一一修改他们的属性值更方便的方法:改变对象属性的默认值。这种方式就是NS-3提供的Config::SetDefault()静态方法。其语法如下:
1 | Config::SetDefault("ns3::ClassName::AttributeName", AttributeValue); |
可见,该方法接受两个参数,第一个参数为属性的路径,其格式为:名称空间::类名::属性名,第二个为属性的默认值,必须是NS-3属性类型(AttributeValue的子类)。
例如下面的程序:
1 | int |
将ns3::MyObject类的MyValue属性的默认值改为500,然后创建了两个MyObject的实例,然后观察其MyValue属性的值。其运行结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
由运行结果可以看出,只要修改了默认值之后,MyObject类所有的实例的MyValue属性全都被默认初始化为500。
2. 属性详解
2.1. 属性值类型
NS-3提供了很多属性值的类型,这些类型都有一个共同的父类:AttributeValue。其申明如下:
1 | …… |
从AttributeValue的申明可以看出:
- AttributeValue可以支持智能指针
- AttributeValue是抽象类,不能创建实例,只能创建其具体子类的实例
- SerializeToString和DeserializeFromString是两个纯虚方法,必须由子类实现
- 所有AttributeValue都提供了转变成字符串的能力
- (几乎)所有AttributeValue都提供了从字符串恢复的能力
2.2. 属性访问器
在创建属性的时候,除了必须明确属性的值类型之外,还必须绑定一个属性访问器,用来确定这个最终存储这个属性值的成员变量究竟是谁。属性访问器的定义如下:
1 | class AttributeAccessor : public SimpleRefCount<AttributeAccessor> |
不同的属性值类型都有自己的访问器实现,这些实现完成了如何设置属性值和读取属性值的必要操作。
2.3. 属性检查器
属性访问器只负责读写属性的值,不负责检查属性的值是否正确。而是使用检查器确保来确保设置到属性上的值符合特殊的要求。这种设计模式符合软件工程中提倡的单一职责原则,即一个类只负责做一件事情。检查器的申明如下:
1 | class AttributeChecker : public SimpleRefCount<AttributeChecker> |
从申明中可以看出检查器最主要的作用就是检查属性是否合法,以及从字符串恢复一个合法的属性值。
2.4. 原始类型的属性值类型
一般来说,属性值类型、属性访问器和属性检查器总是配对出现的。不同的属性值类型总是继承自AttributeValue,例如xxx底层C++类型的属性值类型总是命名为XxxValue,然后总会实现一个配套的XxxAccessor类和一个XxxChecker类与之配套使用。
在NS-3当中,实现了很多的属性值类型。我们通过讲解其中最常用的原始类型的属性值类型来了解NS-3属性值类型的特性。常见的原始数据类型的属性值类型有:
- BooleanValue
- DoubleValue
- IntegerValue
- UintegerValue
- StringValue
2.4.1. BooleanValue
2.4.1.1. BooleanValue的定义
其中BooleanValue是最简单的原始属性值类型。其申明如下:
1 | class BooleanValue : public AttributeValue |
从中可以看出:
- 其底层值类型为bool
- 可以在创建BooleanValue时通过构造函数参数初始化其初始值
- 可以将值转换成字符串,也可以从字符串获得其值
其具体实现为:
1 | std::string |
可见转换成字符串时,其值会变成”true”或者”false”。而从字符串转回BooleanValue时,可以识别”true”、”1”和”t”为真,也可以识别”false”、”0”和”f”为假,如果发现其他值将转换失败。
2.4.1.2. BooleanValue的访问器
由于无需进行特殊的实现,BooleanValue中的访问器是使用NS-3提供的宏ATTRIBUTE_CHECKER_DEFINE()自动生成的:
1 | ATTRIBUTE_CHECKER_DEFINE (Boolean); |
这个宏的定义如下:
1 |
|
可见,这个宏会展开成两个函数:
- MakeBooleanAccessor(BooleanValue a1): 只有一个参数,接受类的成员变量的地址
- MakeBooleanAccessor(BooleanValue a1, BooleanValue a2): 有两个参数,分别接受成员变量的Getter和Setter方法。
我们姑且将第一个函数返回的访问器称为变量访问器,而将第二种函数返回的访问器称为方法访问器。
例如,我们可以使用变量访问器:
1 | MakeBooleanAccessor(&MyObject::m_myBoolValue); |
也可以使用等价的方法访问器:
1 | MakeBooleanAccessor(&MyObject::GetMyBoolValue, &MyObject::SetMyBoolValue); |
来定义BooleanValue的访问器。
2.4.1.3. BooleanValue的检查器
由于BooleanValue无需进行范围等合法性的检查,NS-3默认使用了宏来生成访问器的标准实现,首先在头文件里调用了检查器申明宏模板:
1 | ATTRIBUTE_CHECKER_DEFINE (Boolean); |
这个宏的定义如下:
1 |
|
可见,这个宏展开之后,其实是生成了一个BooleanChecker类和一个MakeBooleanChecker()函数。
这个方法的定义在Boolean类的源文件中通过调用检查器生成宏模板来定义:
1 | ATTRIBUTE_CHECKER_IMPLEMENT_WITH_NAME (Boolean,"bool"); |
这个宏的定义如下:
1 |
|
由此可见,调用MakeBooleanChecker()函数实际上是返回了一个通过MakeSimpleAttributreChecker()函数生成的类,这个类实际上实现通过底层的SimpleAttributeChecker类实现了BooleanChecker类的各种默认方法。这个默认的BooleanChecker类实际上没有做任何实质性的检查,只是判断了一下类型是否兼容。
2.4.1.4. BooleanValue的使用
到此为止,整个BooleanValue类已经定义完成。如果到此为止,还是暂时看不懂究竟是如何实现的,也很正常。这种方式涉及到很多C++模板的高阶应用范式。此处也不追求必须把BooleanValue属性的定义完全看懂,初学阶段只要大概知道如何使用,以及我们用的这些方法都是在哪里定义的即可。等以后要自己写属性值类型时,可以在从最简单的BooleanValue入手分析,然后慢慢理解,以至于最终能够自己写出一个自定义的属性值类型。
接下来,我们通过给前面的MyObject类的例子添加一个布尔类型的属性值来学习如何使用BooleanValue。
首先,MyObject类当中还是需要有一个成员变量(m_myBoolValue)来存储属性值,例如:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
其次,需要在TypeId中添加属性的描述:
1 | TypeId |
由最开始的分析可知,我们可以省略AddAtrribute()方法当中的flags参数,表明该属性支持读写和构造初始化。此外,我们使用的是变量访问器,如果m_myBoolValue具有Getter和Setter方法,我们也可以使用方法访问器:
1 | TypeId |
例子中,属性MyValue的初始值为false。当我们设置属性值的时候是通过调用SetMyBoolValue()方法实现的,而当我们获取属性值的时候是通过GetMyBoolValue实现的。假设我们在GetMyBoolValue()和SetMyBoolValue()方法当中输出一些信息:
1 | bool |
我们运行下面的程序,来验证属性设置的执行过程:
1 | int |
运行该程序得到如下的输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,SetMyBoolValue()方法被调用了两次,第一次是初始化的时候调用的,第二次是我们设置通过调用SetAttribute()执行的。而IsMyBoolValue()方法执行了一次,因为我们调用了GetAttribute()方法。这个例子说明,如果我们的Getter/Setter方法当中实现了一些成员变量操作的业务逻辑,那么当我们使用方法访问器的时候,属性系统完全会遵守这些逻辑;然而当使用变量访问器的时候,会跳过这些业务逻辑对成员变量的值进行修改,这种情况下,容易给属性赋一些不合理的值。变量访问其的执行速度稍稍快一点。具体到底选用哪种访问器,需要按实际情况进行选择。
2.4.2. DoubleValue
如果属性的值是浮点型(float或者double),那么NS-3提供了DoubleValue属性值类型。DoubleValue的实现与BooleanValue基本一致。唯一的区别是DoubleValue的检查器的实现更为丰富。
2.4.2.1. DoubleValue的检查器
DoubleValue除了提供和BooleanValue类似的无参检查其创建函数之外,还提供两个有参的检查器创建函数:
1 | template <typename T> |
通过参数的名字也可以大概猜到这两个检查器创建函数的意义:
- 第一个函数创建的检查器,将检查属性的值,如果属性的值小于某个值,则检查不通过,属性赋值失败
- 第二个函数创建的检查器,将检查属性的值,如果属性的值不在某个范围内,则检查不通过,赋值失败
换句话说,属性的值必须大于等于min并且小于等于max才能赋值成功,如果没有max参数,则不检查属性值的大小。需要注意的DoubleValueChecker可以同时支持单精度和双精度两种浮点值类型,区别在于调用检查器创建函数的时候的时候需要通过模板参数指定具体类型,例如:
1 | MakeDoubleChecker<double>(); |
这么做的原因在于,如果属性的底层变量是float类型,那么将double类型的值设置到属性,有可能存在数值溢出的风险。因此检查器必须先检查类型是否兼容。
2.4.2.2. DoubleValue的使用
首先,向类中添加double类型的成员变量:
1 | class MyObject : public Object //继承自SimpleRefCount,又继承自ObjectBase |
其次,向TypeId中添加属性:
1 | TypeId |
上面的代码中,我们创建的检查器具有两个特征:需要识别类型能不能赋值给double类型;其次,检查类型是不是大于等于5。
我们用以下的程序来验证:
1 | int |
运行程序得到如下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,当给属性赋值小于5时,赋值是无法成功的,程序将中途终止运行。然而遗憾的是,目前NS-3的实现中,错误仅能提示属性设置错误,并提示哪个对象的什么属性赋值错误,然而并不能给出具体的原因(违反了哪个检查器的原则),这也给调试带来了一定的麻烦。
要解决上面的问题,只要将属性值改为大于等于5即可:
1 | int |
即可赋值成功。
除此之外,在NS-3当中,如果DoubleValue不指定最大值,那么检查的最大值将由当前类型所能表达的最大值决定。例如,在我的电脑中,double类型所能表示的最大值为1.7×10^308,超过这个值将无法赋值成功。值得注意的是,在不同的电脑和编译器上,double类型的值的上限可能是不同的,具体要看编译器的实现。
除了对范围进行检查之外,DobuleChecker还将对底层类型进行检查,如果超出底层类型所能表示的范围也将无法赋值成功。例如,我们如果将类型改为float类型:
1 | …… |
那么在设置属性值的时候,如果所设置的值超出了float类型所能表示的范围,也将赋值失败。例如:
1 | int |
将得到运行结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
同样也是提示无法赋值,但是也无法得知具体原因。
2.4.3. IntegerValue和UintegerValue
IntegerValue表示整型属性值类型,而UintegerValue表示无符号整型属性值类型。他们的用法与DoubleValue非常类似:他们的检查器必须指定类型,此外,还可以指定最小值与最大值。如果最大值未指定,那么将有类型的最大值来限制。
整型与无符号整型所能表示的最大原始类型为64位整型:int64_t与uint64_t。而取值范围小于64位的整型类型都能表示,例如:(u)int8_t、(u)int16_t和(u)int32_t等。可以通过检查器创建函数来指定检查的具体类型和检查的取值范围:
1 | MakeIntegerChecker<int8_t>(); |
其具体使用方法和DoubleValue非常类似,在此不再赘述。
2.4.4. StringValue
2.4.4.1. StringValue的定义
StringValue用来表示底层类型为std::string的属性值类型。其定义非常简单,所有类型全是由NS-3的宏自动展开的:
1 |
|
1 |
|
2.4.4.2. StringValue的自动类型转换
StringValue本身并没有特别之处。如果底层类型是std::string类型,都可以用StringValue来将其定义为属性。定义的方法和上面所有的属性类似,其值也无需进行任何检查,只要符合字符串类型标准即可。
然而,NS-3当中的StringValue最大的特色在于,(几乎)任何其他的属性值类型都能使用StringValue表示,并且赋值成功。其原因在于所有属性值类型的父类AttributeValue当中定义了DeserializeFromString虚方法。因此,任何子类都必须实现此方法,也就具备了将字符串转换成具体属性值的能力。
例如,在上面的例子中,我们定义了UintegerValue、BooleanValue和DoubleValue属性。我们可以使用StringValue对这些属性进行设置:
1 | int |
运行程序将得到如下的运行结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可以看出,无论是何种类,都能通过StringValue进行设置与读取。以后我们将会看到,这对于一些复杂的属性值类型的设置将会非常方便。
2.4.4.3. StringValue的手动类型转换
此外,如果有一个StringValue,我们可以通过如下的方式将其转换成想要的属性值类型:
1 | int |
当然,前提是,目标类型和StringValue的值必须兼容。上面例子中使用DoubleChecker来检查,StringValue中封装的值是否符合DoubleValue的要求。
这种方式只适用于将StringValue类型转换成其他属性值类型。如果要将任何其他类型转换为StringValue值类型,可以使用如下的方法:
1 | int |
2.5. 枚举类型的属性值类型
有些变量的取值范围不是连续的,而是离散的,那么可以使用C++的枚举类型来定义。在NS-3中,可以将枚举类型定义为属性的值类型EnumValue。
2.5.1. EnumValue的访问器
EnumValue的访问器和其他属性值类型的访问器没有区别,分为变量访问器和函数访问器两种,分别通过函数:
1 | template <typename T1> |
1 | template <typename T1, typename T2> |
来创建。
2.5.2. EnumValue的检查器
EnumValue的检查器主要用来检查取值是否在枚举类型列表当中,在创建检查器的时候需要传入枚举类型值的列表。NS-3提供了函数来创建EnumValue的检查器:
1 | Ptr<const AttributeChecker> MakeEnumChecker (int v1, std::string n1, |
该函数可以传入21个及以下个不同的枚举类型值,并且每个值都对应一个相应的字符串名字,以后可以直接输出该字符串的名字,以方便调试。此外,这个字符串名字还可以用于使用字符串值类型对属性赋值。
2.5.3. EnumValue的使用
下面的程序演示了如何使用EnumValue值:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
程序首先定义了一个MyObject类,然后在类中定义了一个标准的C++枚举类型UsbTypeEnum,其中包含三个枚举值:Type_A, Type_B和Type_C。然后,程序在MyObject类中定义了一个属性m_usbType,其类型正好是刚刚定义的枚举类型UsbTypeEnum。随后,程序在GetTypeId()方法当中创建TypeId的时候定义了一个枚举值类型的属性TestEnum,该属性的默认值是Type_A。枚举值属性的可选值是由属性检查其确定的。程序中使用静态方法GetUsbTypeEnumChecker()创建了一个EnumValue类型的检查器,同时,这个检查器实例也是静态的。(之所以使用静态方法创建静态变量是因为后面用到该检查器的时候可以直接调用,而不会重新创建。)注意,属性的可选值与枚举类型的值可以不一致,例如可选值可以只传入Type_A和Type_B。在上面的程序中,可选值为:Type_A、Type_B和Type_C。最后在主函数中,我们创建了一个MyObject对象,然后将其TestEnum属性设置为Type_B。设置的过程中,是通过StringValue类型设置的。能够设置成功的原因是NS-3中所有的属性值类型都能使用StringValue表示。然后我们从对象当中取出TestEnum属性值,并将该属性值的转换成字符串输出。将属性值转换为字符串的时候会用到检查器,因此我们需要使用前面创建的静态方法GetUsbTypeNeumChecker()来获取检查器的实例。
对于已经存在的类(例如NS-3已经提供的类,或者第三方类库当中的类,已经编译成.so文件)我们就无法添加GetChecker()方法来返回检查器了。在这种情况下,我们可以写一个辅助函数来从TypeId当中获取相应的检查器:
1 | Ptr<const AttributeChecker> getCheckerByName(TypeId typeId, std::string attributeName) { |
此时,我们的主函数将变为:
1 | int |
两种方式的效果相同。运行程序后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,属性值能够成功的设置与读取。
2.6. 对象指针属性
2.6.1. 单个对象
前面主要介绍的几种属性值类型都是原始数据类型(枚举类型其实也是原始数据类型,因为它主要还是被当做int类型来处理的)。但是在很多时候,有些对象的属性不一定是一个原始的值类型,而是一个其他对象。在这种情况下,就需要使用到对象指针属性PointerValue,来将其他对象的智能指针作为属性值。
PoitnerValue的访问器的创建和其他属性值类型无区别,都是用变量访问器和方法访问器来创建。其检查器和其他类型也一致,但是要使用模板的方法指定其引用的对象类型,例如:
1 | MakePointerChecker <RandomVariableStream>(); |
其中RandomVariableStream就是属性可以设置的对象类型。
我们通过下面的例子来了解PointerValue的应用:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
程序创建了两个类:MyAnotherObject和MyObject,其中MyObject当中具有一个另外一个对象的指针属性,而MyAnotherObject当中具有无符号整形属性。我们可以将MyAnotherObject类的实例通过调用SetAttribute()的方式设置到MyObject类的实例当中,并通过GetAttribute()的方式从当中取出MyAnotherObject的对象。
运行程序之后得到如下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
2.6.2. 多个对象
有的时候,需要在一个对象当中存储其他类型的多个对象,那么便会使用到C++的标准模板库(STL: Standard Template Library)里面的集合类型;而很少会使用数组,因为数组必须事先指定长度,而集合类型的长度可以自动增长。最常用的集合类型主要有:Vector(向量)和Map(映射)两种。
向量是单一值集合,即每个向量元素(entry)都是一个单独的对象(类型)。而映射是键值对(key-value pair)集合,即每一个映射的元素(entry)都是两个强相关的元素:一个键对象(类型),一个值对象(类型)。
在NS-3当中用来表示集合值的属性类型是ObjectPtrContainerValue,其主要作用是存储多个对象的智能指针。为了匹配C++当中的集合类型,NS-3将ObjectPtrContainerValue重定义成了两种具体的类型:ObjectVectorValue和ObjectMapValue。并对两种不同的具体类型提供了不同的方法支持。
2.6.2.1. ObjectVectorValue
如果底层存储使用的是vector集合类型,那么可以将属性定义为ObjectVectorValue类型。ObjectVectorValue实际是是ObjectPtrContainerValue的一个别名:
1 | typedef ObjectPtrContainerValue ObjectVectorValue; |
但是却提供了一些和vector相关的方法,方便使用。
和前面介绍的其他属性之类型类似,ObjectVectorValue也同时提供了两种访问器:变量访问器和函数访问器。变量访问器的使用和其他类型的变量访问器一致。而函数访问器稍有差别。原因在于其他单对象访问过程中,只需要使用对应的Getter/Setter方法即可。而在vector值类型中,函数访问器,中使用的方法不再是Getter和Setter方法,其中第一个方法应该返回vector中元素的个数,而第二个方法应该返回第i个元素。例如下面的代码片段所示:
1 | uint32_t DoGetVectorN (void) const { return m_vector2.size (); } |
其中DoGetVectorN()返回了vector当中元素的个数,而DoGetVector()返回了vector的第i个元素。那么在定义vector属性的时候,可以使用如下方法:
1 | .AddAttribute ("TestVector2", "help text", |
从中可以看出,ObjectVectorValue属性,更多的是获取属性的值,而未提供设置方法。实际上,它底层的ObjectPtrContainerValue也未提供任何设置方法的实现:
1 | bool |
因此,基本可以认为ObjectVectorValue属性是一个只读属性:通过属性的方式仅仅能读取其中的值,如果想改变vector当中的元素值,或者改变vector元素本身,那么必须改变对象中的成员变量本身,此时,对应的属性值也会改变。
我们用下面的例子来演示ObjectVectorValue的使用:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
运行程序后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
从中可以看出:
- vector当中的对象一定是以智能指针Ptr来表示的
- vector当中的对象一定继承自NS-3的Object类
- vector属性仅具有读取的功能
- 无法设置vector类型的属性
- 每次改变vector后,必须重新获取属性才能得到修改后的属性
此外,还需要注意的是,vector属性是无法用StringValue属性值创建的:
1 | bool |
2.6.2.2. ObjectMapValue
和vector类似,有时候我们在一个对象中底层存储其他对象的时候,使用的底层数据类型是map,那么可以使用ObjectMapValue来表示。
和ObjectVectorValue类似,ObjectMapValue类型也是一个ObjectPonterContainerValue的别名。同样提供了一套适用与Map的函数。
ObjectMapValue具有和ObjectVectorValue类似的特性:
- 是只读属性
- 改变之后要重新获取属性
- 无法从StringValue创建
- Map当中的值必须是Object类,并且必须使用Ptr智能指针表示
此外,ObjectMapValue当中的键必须是整型,不能是其他类型。(这在一定程度上限制了映射类型的应用范围,并且不符合STL的多类型支持特性,不知NS-3会不会在以后进行扩展。)
我们通过以下程序来演示ObjectMapValue属性的使用:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
程序中定义了两个类:MyObject和MyAnotherObject。前者维护了一个后者的映射,键值是uint32_t类型。随后MyObject在TypeId当中定义了一个属性myMap,该属性使用了函数访问器,并传入两两个方法:第一个方法获取map中的元素,第二个方法获取map中元素的个数。这两个方法的位置可以交换。
在实现获取map中元素的时候,需要注意两点:
- 方法必须是const方法,这是MakeObjectMapAccessor()方法当中的规定。
- 如果键值对应的对象为空,需要返回空指针。因此,可以使用map.find()和map.at()两种方法实现。其中后者在未找到对象时,会抛出异常。具体请参阅C++中STL,在此不再赘述。
MapObjectMapChecker()方法需要用模板的形式传入map中所能接受的对象。map其他的用法和vector非常类似,可以对照学习。
3. 对象工厂
在NS-3中,我们现在使用其对象和属性框架的步骤一般如下:
- 使用CreateObject<>()方法创建某个类的对象实例,并得到一个指向该对象的智能指针。
- 使用对象的SetAttribute()方法来设置对象的各种属性。
- 调用Initialize()方法来初始化对象,因为SetAttribute()方法设置的属性,无法在构造函数中初始化。
然而有些时候,我们需要创建大量很多属性都相同或者相似的对象。一种方式是,一个个创建对象,并依次设置它们的各种属性。很显然这种方法非常繁琐。另外一种方式是,将对象创建成为数组或者集合,然后使用循环来创建它们。当然这种方式仅支持对象数组或者对象集合。
当对象即不是数组又不是集合的时候,我们还可以使用另外一种方式,就是前面提到过的使用Config::SetDefault()方法,改变所有对象的属性的默认值,然后再开始使用CreateObject()函数创建对象。这种方法相对简单,然而,如果我们不是想改变所有对象的属性值,而只改变一些对象的默认值,在创建完对象之后还是要通过Config::SetDefault()方法将所有属性还原(意味着我们之前还要先记下原来的默认值是什么)。
3.1. 对象工厂的使用
NS-3提供了另外一种机制来批量的创建拥有同样属性的对象:对象工厂。对象工厂实际上是一个类ObjectFactory。它是NS-3对象框架应用的极致体现。ObjectFactory的定义如下:
1 | class ObjectFactory |
从类的定义可见,ObjectFactory使用非常简单:
- 创建一个ObjectFactory。
- 如果构造函数未指定TypeId,则可以通过SetTypeId()方法指定一个需要创建的对象的TypeId。
- 使用Set方法设置对象的属性值,如果属性在该TypeId当中不存在,则将抛出异常。
- 反复调用Create()方法创建对象,这些对象都具有同样的属性值。
此外,NS-3还提供了另外一个实用的函数,可以在创建Object的同时指定属性,可以同时指定1到9个属性:
1 | template <typename T> |
我们用下面的例子来解释ObjectFactory类的使用:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
上面的程序使用ObjectFactory创建了三个对象,然后比较了三个对象是不是同一个对象,然后输出了三个对象的三个不同的属性值。运行程序之后,得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,使用ObjectFactory三次创建出的对象均不是同一个对象,但是三个对象却拥有同样的属性值。因此,使用ObjectFactory非常适合批量地创建属性值相同的对象。
3.2. 从StringValue创建对象
本章前面提到几乎所有的属性值类型都能够从StringValue创建,包括原始数据类型的属性值类型。前几节也提到过,多值对象属性ObjectVectorValue和ObjectMapValue是不可以通过StringValue创建的。那么前文并未提及单值对象PointerValue是否能够通过StringValue创建呢?如果可以,字符串又该写什么才能创建出指向该对象实例的智能指针呢?
答案是肯定的:可以通过字符串值类型创建PointerValue属性值。并且字符串写的就是想要创建的对象的TypeId的名字。例如:
1 | obj->SetAttribute("myPointer", StringValue("ns3::MyAnotherObject")); |
这样myPointer属性便会指向一个MyAnotherObject的实例。
实际上,StringValue转换成PointerValue底层是通过ObjectFactory来实现的。字符串值即为调用ObjectFactory的SetTypeId()方法的参数。既然ObjectFactory可以初始化属性,那么StringValue也是可以指定初始化属性的。NS-3当中,使用[]来表示属性。其中使用=来分割属性名称和属性值。所有属性值都被认为是字符串,因此无法给无法从StringValue转换的属性值类型初始化。字符串初始化属性的方式如下:
1 | obj->SetAttribute("myPointer", StringValue("ns3::MyAnotherObject[intValue=5]")) |
如果要设置多个属性,则属性和属性之间使用竖线|来分割:
1 | obj->SetAttribute("myPointer", StringValue("ns3::MyAnotherObject[intValue=5|doubleValue=2.0]")) |
实际上,属性的设置也是通过ObjectFactory来完成的。PointerValue的DeserializeFromString()方法找到方括号[]之间的内容,然后用|区分不同的属性,然后通过=区分属性的名字和值。最后通过ObjectFactory的Set()方法将属性和值设置到ObjectFactory当中。然后调用Create()方法完成对象的构造。
例如如下代码:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
代码中,我们创建了两个类MyObject和MyAnotherObject,前者拥有一个后者的指针,并定义了相应的属性MyObject。后者拥有两个属性:整型属性myValue和浮点型属性myDouble。程序中,我们使用ObjectFactory对象设置了MyObject的TypeId,然后使用Set()方法设置了myObject属性为一个字符串值类型,取值为:”ns3::MyAnotherObject[myValue=5|myDouble=25.5]”,即myObject将创建一个MyAnotherObject对象,该对象拥有两个属性值分别是5和25.5。
该程序的运行结果如下:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见对象可以通过字符串值类型创建,并且正确初始化属性值。
4. 自定义属性值类型
很多时候,我们需要设置的属性不是原始数据类型,而是自己新创建的类。如果我们创建的类继承自NS-3的Object类,那么便可以使用PointerValue来引用。当然,Object类是需要许多代价的。有时候,一些简单的类型必须继承自Object类是没有必要的。如果这个简单类型又必须要加入属性系统,我们可以使用自定义属性值类型的方法。
接下来,我们通过自定义一个三角形类来演示自定义属性值类型的方法。
4.1. 定义数据类型
任何的属性值类型,底层一定需要一个成员变量来存储。因此第一步必须创建一个用于存储这个值的底层类型,这个类型一般是普通C++类:
1 | class MyTriangle { |
4.2. 定义属性值类型
有了底层的数据存储类型之后,还需要定义一个属性值类型,这个类型必须继承自AtrributeValue,名字一般是底层类型加Value,例如我们的例子就是MyTriangleValue。然而,自己写属性值类型会比较繁琐,NS-3中提供了两个宏来帮助我们申明和定义一个属性值类型:
1 | //生成属性值类型的申明,type是底层存储类型的类名 |
这两个宏的使用非常简单,例如我们要为新创建的MyTriangle类生成属性值类型,可以:
1 | ATTRIBUTE_HELPER_HEADER(MyTriangle); |
第一个宏ATTRIBUTE_HELPER_HEADER(MyTriangle)帮我们生成了MyTriangleValue类的申明、两个默认的MakeMyTriangleAccessor()方法以及一个MakeMyTriangleChecker()方法。宏展开后,对应的代码如下:
1 | class MyTriangleValue : public AttributeValue \ |
而第二个宏ATTRIBUTE_HELPE_CPP(MyTriangle)即实现了MyTriangleValue这个类以及对应的MakeMyTriangleAccessor()方法和MakeMyTriangleChecker()方法。宏展开后,对应代码如下:
1 | Ptr<const AttributeChecker> MakeMyTriangleChecker (void) { \ |
实际上,在最简单的情况下,使用了这两个宏之后,自定义的属性值类型已经可以使用了。
4.3. 实现和使用属性
完整程序如下:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
在具体实现属性的时候需要注意的是:
- 一般会提供一个输出方法,来输出属性的值,以方便调试。在这个程序中,我们重载了对于输出流的<<操作符,打印了三角形的三个属性。
- 如果是输出过程需要用到数据类的私有成员,则需要将操作符定义为数据类的友元方法。
- 必须提供一个输入方法,来从输入流输入属性的信息。程序中我们重载了输入流的>>操作符,用来处理输入,程序中暂时没有进行任何处理。
4.4. 和字符串之前转换
前面在介绍NS-3属性框架的时候介绍过,属性值类型和字符串值类型之前可以进行转换。默认情况下,属性值向字符串转换的功能主要是依靠操作符<<来实现的。这个在我们的程序当中已经实现了。而从字符串转换成特定属性值类型主要是依靠操作符>>来实现的。目前我们程序当中并未实现。
4.4.1. 指定类型表示规则
我们首先得定一个规则,如何使用字符串来表示一个属性。由于我们的MyTriangle类并没有继承自Object类,因此无法直接通过ObjectFactory来转换,而需要自己实现转换方法。考虑到我们的底层类是一个三角形,那么我们可以输入三个浮点数来表示三条不同的边,数字之间使用逗号分隔。
4.4.2. 编写解析代码
定好了解析规则之后,我们就可以开始写解析程序,在MyTriangle中加入静态方法DoParse():
1 | class MyTriangle { |
在解析参数的过程中,我们需要保证:
- 以逗号分隔的数字个数必须是3个
- 每个数字必须能够转换成double类型
- 满足以上两个条件,返回true
- 不满足任意一个条件,返回false,表示参数解析失败
最后,在>>操作符中调用DoParse()方法,即可自动完成类型转换:
1 | std::istream &operator >> (std::istream &is, MyTriangle &triangle) { |
如果参数解析失败,则DoParse()方法必然返回false。因此,我们可以使用断言(Assert)来确保,输入的参数必须是正确的,否则,程序将会停止运行,并输出错误。
用以下程序来测试类型的转换(注意在这种方式下,参数的各个部分之间不能又空格):
1 | int |
程序中,我们使用StringValue来给MyTriangleValue赋值。NS-3的属性系统会自动调用转换程序,从字符串来构造三角形。注意这种模式下,参数的各个部分之间不能留有空格,例如”5,3,2”不能写为”5, 3, 2”(当然,要允许空格也可以简单地修改程序来适应,在此不再赘述)。运行程序之后,可以看到输出结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
当然,如果,我们提供的参数不正确,程序便会异常终止。例如,我们将用于构造三角形的字符串改为:
1 | obj->SetAttribute("myTriangle", StringValue("5,3,")); |
将得到运行结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可以看到明确的assert failed提示,并且获得提示信息以及错误发生的位置。
4.5. 检查属性的正确性与自定义检查器
如果我们仔细观察,我面上面举的例子当中的三角形”5,3,2”其实并不是一个三角形。我们必须在创建三角形属性的时候检查传入的参数是否真的能构成一个三角形。
回顾:三角形的构成条件:两边之和必须大于第三边,两边只差必须小于第三边。换句话说,我们可以这么理解:
- 任意从三边当中取出两边相加,它们的和必须大于第三边。这是一个典型的组合问题,一共有$C_{3}^{2}=3$种可能,依次为:
- $a + b > c$
- $a + c > b$
- $b + c > a$
- 任意从三角形当中取出两边相减,它们的差必须小于第三边。这是一个典型的排列问题,一共有$P_{3}^{2}=6$种可能:
- $a - b < c$
- $b - a < c$
- $a - c < b$
- $c - a < b$
- $b - c < a$
- $c - b < a$
我们可以在创建三角形的时候检查这三九个条件,如果其中任意一个不满足,那么三角形构造失败,将三角形标记未非法。但是这种方法当中,if语句的条件未免显得太复杂了。我们考虑简化这个情况。实际上,我们只要检查最短的两条边的和是否大于最长的边,即可满足这9个条件。下面我们给出证明:
证明:首先,不失一般性地,我们假设最长边是$c$。
因此有$c \ge a$且$c \ge b$,又由于$a, b, c \ne 0$,因此可以推出:$c + b > a$和$c + a > b$。
由于最短两条边之和大于最长边,有:$a + b > c$。第一组条件成立。
由$a + b > c$可以推出:$c - a < b$和$c - b < a$。
由$c + b > a$可以推出:$a - b < c$和$a - c < b$。
由$c + a > b$可以推出:$b - c < a$和$b - a < c$。
因此,第二组条件成立。证毕。
然而,我们需要从三个数当中最小的两个,这需要排序,复杂度较高。我们可以考虑另外一种方法:找到最大值,然后全部求和,再减掉最大值即剩下两个最小值之和:
$$
m = max(a,b,c) \\
a + b + c - m ?> m
$$
即,在保证a,b,c均为正数的情况下,程序只需要判断如下条件即可:
1 | double m = max(a, b, c); |
可以在MyTriangle类当中添加一个静态方法Vlidate()用来检查参数是否合法:
1 | class MyTriangle { |
然后,在MyTriangle类当中加入一个validate属性,默认为false。只有验证通过了才为true:
1 | class MyTriangle { |
因此构建的时候,只要不符合条件,构造出的三角形一定不合法。如果我们想限制只有合法的三角形对象才能作为属性设置,那么必须重写MyTriangleValue类的检查器。默认情况下,检查器是由ATTRIBUTE_HELPER_CPP()宏自动展开的。前面查看宏的定义可以发现,该宏实际上又是两个宏定义的:
- ATTRIBUTE_CHECKER_IMPLEMENT():定义了检查器的实现
- ATTRIBUTE_VALUE_IMPLEMENT():定义了类的实现
- 访问器是模板方法,是在HEADER当中定义的
由于我们需要自定义检查器,因此不能包含检查器的默认实现,因此仅仅需要定义类的实现即可。而具体检查器的实现有用户自己实现。下面的程序演示了如何自定义检查器的实现的完成程序:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
我们运行程序之后有如下的结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,我们通过程序检查了数据的合法性,如果传入的三角形不合法,是无法被设置成属性的。
其中值得注意的是interval名称空间当中实现的方法,内部内嵌了一个结构体(相当于一个内部类),这个类完成了实际的AttributeChecker的工作。之所以采用这个方式是为了让MyTriangleChecker通过不同的方法创建的时候能拥有不同的具体实现。如果我们直接让MyTriangleChecker实现这个检查方法,那么它便无法再有其他的实现可能。我们通过下面的例子来解释,为了使用内部类可以让MyTriangleChecker有多种实现。
假设我们有时候还要限定三角形的类型是以下类型之一:锐角三角形(所有角均小于90度)、钝角三角形(有一个角大于90度)或者直角三角形(又一个角等于90度)。那么我们可以申明另外一个方法,这个方法应该传入我们限定的三角形类型:
1 | …… |
这样,MakeMyTriangleChecker()应该要返回另外一种类型的类,这个类当中需要判断三角形的类型参数type。因此,我们还是选择使用内部类的实现,完成的程序如下:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
可见internal名称空间当中有两种MakeMyTriangleChecker的实现,分别使用内部类实现了有参数和无参数的两种检查器。程序中定义了属性值必须是直角三角形,并且在给属性复制时通过检查器当中进行了检查,如果检查不通过,则属性设置将无法完成。运行程序之后得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
如果我们传入的值不是直角三角形,例如传入3,4,6我们将得到如下的结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
此外,我们修改检查器参数为钝角三角形,并传入钝角三角形参数2,3,4,则将得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
如果我们传入一个锐角三角形6,6,6,则将得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,我们自己实现的检查器确实起到了检查属性的作用。
4.6. NS-3当中两种自定义属性值类型
4.6.1. 时间类型Time
时间类型是最常用的自定义NS-3类型,主要用于表示仿真的时间。时间类型使用Time类来表示。Time类能够表示一共10中精度类型,默认为秒纳秒(NS)级别:
1 | enum Unit |
可以通过调用Time类的静态方法SetResolution()来改变默认的级别。
1 | static void SetResolution (enum Unit resolution); |
设置时间表示的精度后,可以通过以下静态方法来创建时间类型:
1 | static Time From (const int64x64_t & value); |
或者在使用静态方法构造的同时传入时间精度:
1 | static Time FromInteger (uint64_t value, enum Unit unit); |
同时Time类也提供了丰富的构造函数可以调用:
1 | explicit inline Time (double v); |
除此之外,NS-3还提供了更加直观的函数来创建时间实例,这些函数可以直接调用,并返回相应的时间实例:
1 | inline Time Years (double value); |
时间类型也能够被转换成相应单位的普通数据类型,以方便输出:
1 | inline double GetYears (void) const; |
此外,NS-3还提供了很多关于时间的操作符,例如基本的加减乘除等:
1 | friend bool operator == (const Time & lhs, const Time & rhs); |
以及各种工具方法:
1 | friend Time Abs (const Time & time); |
时间类型也能通过属性值类型宏被扩展成属性:
1 | ATTRIBUTE_VALUE_DEFINE (Time); |
这两个宏会扩展出默认类型TimeValue,以及默认的变量访问器和函数访问器方法MakeTimeChecker()。
此外,NS-3定义了三种Time属性值类型的检查器:
1 | Ptr<const AttributeChecker> MakeTimeChecker (const Time min, const Time max); |
4.6.2. 速率类型DataRate
速率也是NS-3的基本自定义类型之一,用于表示数据的传输速率或者带宽。速率使用DataRate类表示。和时间类一样,速率也可以使用不同的单位来表示。速率的基本单位为bps(bit per second),这也是默认的速率存储单位,任何其他单位都是bps的整数倍。DataRate提供了一个构造函数以bps为基本单位:
1 | DataRate (uint64_t bps); |
除此之外,NS-3也提供了使用字符串来构造DataRate类的构造函数,支持以数字和单位来表示速率:
1 | /** |
可以看出,其支持非常丰富的单位类型。其中小写字母b表示bit,而大写字母B表示字节。而倍数关系主要分为十进制倍数,例如K和二进制倍数例如Ki,它们的关系如下:
Prefix | Value |
---|---|
“k”, “K” | 1000 |
“Ki” | 1024 |
“M” | 1000000 |
“Mi” | 1024 Ki |
“G” | 10^9 |
“Gi “ | 1024 Mi |
DataRate类也提供了基本关系运算符用于比较两个速率之间的关系:
1 | bool operator < (const DataRate& rhs) const; |
也可以获取底层的bps速率:
1 | uint64_t GetBitRate () const; |
此外,DataRate类还提供了方法计算以该速率传输一定量的数据所需的时间,并返回Time类的实例:
1 | Time CalculateBytesTxTime (uint32_t bytes) const; |
这两个函数的名字都已经很直白了,在此就不再赘述。此外,还要注意,老版本的NS-3当中具有如下方法,为了保持兼容性,暂时还未删除,但现在已经不再推荐使用(猜测废弃的主要原因是无法区分数据量的单位):
1 | double CalculateTxTime (uint32_t bytes) const; |
同时,NS-3也提供了如下方法,可以让DataRate类和Time类相乘计算数据量:
1 | double operator* (const DataRate& lhs, const Time& rhs); |
和Time类类似,DataRate类也通过标准的属性值宏定义为了属性值类型:
1 | ATTRIBUTE_HELPER_HEADER (DataRate); |
因此,它具有默认的DataRateValue,MakeDataRateAccessor()和MakeDataRateChecker()方法。