脚本宝典收集整理的这篇文章主要介绍了;,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
前言
NS3作为一个网络仿真库,出于性能的考量选择了C++。在写仿真程序时,不可避免的要对各种实体进行建模,自然C++中的class成了唯一可选的方案。不加任何技巧的class的确可以满足对某些实体的建模,可是在仿真软件的编写中需要有足够的动态性,比如有这样一些需求:
- 动态的获知某个实体所具有的各类属性与属性的值
- 这个实体的状态变化后引发一系列的动作
这些都不是过分的需求,如果真的写过仿真程序的话肯定会非常渴求使用的软件能够提供实现这些需求的方法。要自己干巴巴的实现这些需求也不是不可以,比如可以提供一些查询接口来实现1;对于2的话,Qt的signal/slot或许可以实现。说到Qt了,其实QObject拥有了超越普通C++ class的能力,也都能满足上面指出的这些需求,但是其解决方案似乎有点重。
幸好,NS3通过TyPEId可以很好的解决上面提出的各类需求。
Typeid
是什么
class TypdId
{
uint16_t m_tid;
}
这就是TypdId,就是这么简单。似乎有些不可思议,但TypdId
本身只是一个16bIT的整型,之前提到的所有的复杂功能都是藏在TypdId
背后的IidManager
完成的,这个m_tid
只是作为一个索引而存在。
TypdId提供了非常多的方法,比如说增加一个属性(AddAttribute
),增加一个TraceSource(AddTraceSource
),这些方法只是直接了当的将所需信息搜集起来转发给IidManager
。可以看个例子:
TypeId
TypeId::AddAttribute (std::string name,
std::string help,
uint32_t flags,
const AttributeValue &initialValue,
Ptr<const AttributeAccessor> accessor,
Ptr<const AttributeChecker> checker,
SupportLevel supportLevel,
const std::string &supportMsg)
{
NS_LOG_FUNCTION (this << name << help << flags
<< &initialValue << accessor << checker
<< supportLevel << supportMsg);
IidManager::Get ()->AddAttribute (m_tid, name, help, flags,
initialValue.Copy (), accessor, checker,
supportLevel, supportMsg);
return *this;
}
基本上所有的TypdId
的方法都是这个样子。所以解决问题的核心其实是IidManager
。IidManager
可认为是一个类型数据库,保存了与TypdId
想关联的Attribute与TraceSource。具体的内部实现就太细节了,作为使用方是不需要也不应该去关注的。
从使用角度看TypdId
正如在Qt中一样,想要使自己写的一个类拥有强大的能力,需要自己动手在类的声明中添加Q_OBJECT。在NS3设计的TypeId
系统中,这个步骤是要给自己的class添加一个静态方法static TypeId GetTypeId (void)
,然后在这个函数里返回一个TypeId
。在这个过程,可以尽情的使用TypeId
提供的各种方法来给本类加属性和TraceSource,唯一的限制就是这个返回的TypeId
应该是处于GetTypeId
里的静态变量,这是为了保证全局的唯一性。当然了,写C++的限制多了去了,这个规则应该归入这个NS3库的使用方法吧,不太值得吐槽。
TypeId
对于我们平时写程序的最大的帮助在于,它可以给自己的类添加Attribute和TraceSource。
Attribute可以代表该实体的一些属性,比如说一台PC的IP地址、一只猫的体重等。你可能会想,这个不就是一个Get函数的事儿么,值得专门搞这么一套复杂的系统么。其实还真值得:你会去写一个127.0.0.1还是2130706433?在NS3里,可以直接写127.0.0.1,这也得归功与这个Attribute系统。
TraceSource可类比Qt的Signal,在需要的时候调用这个Functor(想不到更好的名称了,不过写C++的应该都知道这个东西),连到这个TraceSource的其他函数(所谓的Slot)就会被自动调用。好处自不必多说,要知道Qt能得到广泛的认可,Signal/Slot功不可没,NS3里的TraceSource系统就是Signal/Slot的翻版。
还有一个使用限制就是,需要通过一个宏来告知系统我一个TypeId
要注册:NS_OBJECT_ENSURE_REGISTEred
。这个宏其实声明了一个静态类并同时声明了一个本文件内的静态实例。在这个静态类的构造函数中调用了我们定义的类的GetTypeId
,这就实现了自定义TypeId
的注册。
Attribute与TraceSource
终于到重头戏了。其实这两部分的代码都切实的体现了C++罪恶的地方:模板与宏。一个新手要看懂这些代码要补充太多的东西了。说来惭愧,其实我自身也是一个新手,从开始接触这个库到现在能初步搞明白这样一个系统(真的只是大致初步明白),已经过去了3年多。这个系统的实现是模板里套宏、宏里套模板,看的时候需要时刻注意这段代码是宏生成的代码还是用了模板。
Attribute
前面提到了我们可以用127.0.0.1来代表ip的那32个bit,这就是Attribute系统的神奇所在。在这个例子里,127.0.0.1其实是一个字符串,Attribute系统可以把字符串转化为任何一种类型(可以是自定义类型)。
就单纯的以这个地址类型来解释好了。我们的程序中需要使用IP地址,其最合适的存储方式其实是一个int,但IP地址最适合人类的表述方式其实是点分表示,我们自然也想在使用的时候用这种方式。那这个应该怎么做?
首先先不管人怎么读写的问题,先考虑程序中的这个属性的使用方式。作为一个ipv4的值,肯定有一些相关联的方法,比如说是否为广播地址、是否为多播地址、是否为本地地址之类类的。这些可以以用成员函数的方式实现,既然这样,那就尽情的实现吧!不需要考虑怎么集成到Attribute系统中去。同理,这个类里面有什么字段,想要什么字段就尽情的加。想必你也看出来了,我们在实现一个Attribute的时候,其实根本不需要考虑什么集成的问题。
能够用ns3的方式来给一个对象设置属性的这个能力依赖与3个基本的组件
AttributeValue
AttributeAccessor
AttributeChecker
首先看看什么是ns3的方式为一个对象设置属性,看一下官方manual里的例子
Ptr<configExample> a2_obj = CreateObject<ConfigExample> ();
a2_obj->SetAttribute ("TestInt16", IntegerValue (-3));
IntegerValue iv;
a2_obj->GetAttribute ("TestInt16", iv);
第一行创建了新的对象ConfigExample
,并存在指针a2_obj
里。第二行就是所谓的ns3的方式设置属性,依赖于一个方法SetAttriute
。这个方法属于ObjectBase
,所有能用Ptr
指向的对象都是objectBase
的子类。所以说,在调用SetAttribute
时,除去C++的语法糖,这句话完整的形式是这样的:
@H_512_227@SetAttribute (a2_obj, "TestInt16", IntegerValue (-3));
好了,我们跳进去看看实现
void
ObjectBase::SetAttribute (std::string name, const AttributeValue &value)
{
NS_LOG_FUNCTION (this << name << &value);
struct TypeId::AttributeInformation info;
TypeId tid = GetInstanceTypeId ();
if (!tid.LookupAttributeByName (name, &info))
{
NS_FATAL_ERROR ("Attribute name="<<name<<" does not exist for this object: tid="<<tid.GetName ());
}
if (!(info.flags & TypeId::ATTR_SET) ||
!info.accessor->HasSetter ())
{
NS_FATAL_ERROR ("Attribute name="<<name<<" is not settable for this object: tid="<<tid.GetName ());
}
if (!DoSet (info.accessor, info.checker, value))
{
NS_FATAL_ERROR ("Attribute name="<<name<<" could not be set for this object: tid="<<tid.GetName ());
}
}
这个方法是对象自己身上的方法,所以要记住this
这时候指向的是谁:这里就是a2_obj
。这个方法也很直白
GetInstanceTypdId()
拿到真正的、ConfigExample
的TypeId
AttributeInformation
,就有了accessor
、checker
,还有作为参数传进来的值value
。DoSet
做了实际的设置工作再看看DoSet
:
bool
ObjectBase::DoSet (Ptr<const AttributeAccessor> accessor,
Ptr<const AttributeChecker> checker,
const AttributeValue &value)
{
NS_LOG_FUNCTION (this << accessor << checker << &value);
Ptr<AttributeValue> v = checker->CreateValidValue (value);
if (v == 0)
{
return false;
}
bool ok = accessor->Set (this, *v);
return ok;
}
检查什么的就不说了,最让人关心的是这个方法accessor->Set (this, *v)
。这个方法是怎么定义的,是哪里来的?这下欢迎进入模板与宏的世界。
AttributeAccessor
答案是这个这个方法是属于accessor
的,而accessor
的定义是在注册TypeId的时候生成的。RTFSC:
class ConfigExample : public Object
{
public:
static TypeId GetTypeId (void) {
static TypeId tid = TypeId ("ns3::A")
.SetParent<Object> ()
.AddAttribute ("TestInt16", "help text",
IntegerValue (-2),
MakeIntegerAccessor (&A::m_int16),
MakeIntegerChecker<int16_t> ())
;
return tid;
}
int16_t m_int16;
};
NS_OBJECT_ENSURE_REGISTERED (ConfigExample);
看到那句MakeIntegerAccessor (&A::m_int16)
了么?搞懂了这个,其实就能搞懂ns3的套路了,再看其他的机制也就顺风顺水了。我们慢慢来,一步一步来,保证每一步都有始有终,不会出现跳跃的现象。这个过程稍微有点冗长,可以去拿包零食边吃边看了。
MakeIntegerAccessor
是可调用的一个“东西”。回想一下C++可调用的东西有哪些?1. 函数,2. Functor,就是实现了自定义operator()的一个class的实例。3.实例化一个类型看起来也像是函数调用。我用的Eclipse,f3跳转到定义,等我过去的时候傻眼了:
ATTRIBUTE_ACCESSOR_DEFINE (Integer);
好家伙,看来要展开这个看看了,ctrl+=让它现形:
template <typename T1>
Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1)
{
return MakeAccessorHelper<IntegerValue> (a1);
}
template <typename T1, typename T2>
Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1, T2 a2)
{
return MakeAccessorHelper<IntegerValue> (a1, a2);
}
这展开了2个函数,到这时可以确定,MakeIntegerAccessor
是一个函数,而且我们调用的是只有一个入参的那个函数,这个函数返回了一个AttributeAccessor
的智能指针。具体的展开过程就不细讲了,也没有讲的必要,看看ATTRIBUTE_ACCESSOR_DEFINE
的定义就明白了。现在需要关心的是我们现在调用的函数里有个T1
,要搞明白这个T1
的类型是什么。
重新回头看看MakeIntegerAccessor (&A::m_int16)
,这里的T1
就是&A::m_int16
的类型。先就此打住,这个结论先记下来。我们继续追下去,这下应该看真正的实现MakeAccessorHelper<IntegerValue> (a1)
:
// 第一种实现
template <typename V, typename T, typename U>
inline
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (U T::*memberVariable)
// 第二种实现
template <typename V, typename T, typename U>
inline
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (U (T::*getter)(void) const)
// 第三种实现
template <typename V, typename T, typename U>
inline
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (void (T::*setter)(U))
结果就是匹配到了第一种实现。
其实我曾经很多次追到了这里,却没看懂这里的类型到底是什么意思。也不知道什么时候忽然就明白了。A::m_int16
对应于U T::*
,是个正常人第一眼看上去绝对不会明白这到底是怎么联系在一起的,我也是正常人,所以我现在也不明白这种怪异的语法到底是谁第一次使用的。T
对应于A
,那么U
应该是对应于m_int16
。这个类型能代表一个类里的一个成员变量的类型,T
表明了它是一个类的成员变量,U
表明了这个变量的类型是uint16_t
,现在就只能这么死记了,要想真正搞明白我觉得应该去翻一下编译器里前端到底是怎么解析这个鬼畜般的语法的,先就这么囫囵吞枣吧!对于另外的两个反而更好懂一点,那个类型和平时用的函数指针类型声明挺像的,反而不用多说。一个是getter
,说明这个attribute只提供了获取的接口;一个是setter,说明这个attribute
只能设置不能获取。当然了,这是站在ns3的使用方式上说的,直接强行用c++的方式赋值不在我们的讨论范围之内。
这3个函数都返回了一个指向AttributeAccessor
的指针。现在来看看实现吧!
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (U T::*memberVariable)
{
/* AttributeAcessor implementation for a class member variable. */
class MemberVariable : public AccessorHelper<T,V>
{
public:
/*
* Construct from a class data member address.
* param [in] memberVariable The class data member address.
*/
MemberVariable (U T::*memberVariable)
: AccessorHelper<T,V> (),
m_memberVariable (memberVariable)
{}
private:
virtual bool DoSet (T *object, const V *v) const {
typename AccessorTrait<U>::Result tmp;
bool ok = v->GetAccessor (tmp);
if (!ok)
{
return false;
}
(object->*m_memberVariable) = tmp;
return true;
}
virtual bool DoGet (const T *object, V *v) const {
v->Set (object->*m_memberVariable);
return true;
}
virtual bool HasGetter (void) const {
return true;
}
virtual bool HasSetter (void) const {
return true;
}
U T::*m_memberVariable; // Address of the class data member.
};
return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false);
}
照样很鬼畜。这是在函数里定义了一个类,并且返回了指向这个类的只能指针。这个类继承自AccessorHelper
,而AccessorHelper
又继承自AttributeAccessor
。所以将其作为AttributeAccessor
的子类返回也说得过去。
至于为什么要继承这么多?我现在的理解是这样的
AttributeAccessor
只是一个纯虚接口,它只定义了作为Accessor应当具有的接口。在Java里的话,估计这就是个Interface。AccessorHelper
提供了Set
和Get
的默认实现,把一些可变的部分留给了它的子类来实现,这些可变的部分是DoSet
和DoGet
。所以在MemberVariable
要实现DoSet
和DoGet
。这应该是某种设计模式,看看那本书就能找到了。到现在为止,我们知道可以造出来一个AttributeAccessor
,并把指向这个AttributeAccessor
的指针存在了我们的IidManager
的数据库中。以后想要拿出来这个AttributeAccessor
,就要手拿TypeId
去找IidManager
去要,而且要到的也是一个指针,这个指针指向了在return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false);
这句话里的那个new出来的地址。
总结一下,一个类型的AttributeAccessor
只有一个,就是那个new出来的地方。程序其他地方都是拿指针去访问的。在那块地址存的东西只有两样(只考虑我们现在这个membervariable类型的accessor)
U T::*m_memberVariable
的值,这个值代表了这个变量在拥有TypeId
那个类里的偏移量AttributeAccessor
的实例是有虚表指针的,这个虚表里就是真正的、对应类型的函数实现。回头看看那个DoSet
,里面那个accessor到底是什么应该已经清楚了。那个个accessor的Set
方法在哪儿定义的?答案是AccessorHelper
。我直接把结论公开了,但是你现在应该停下来去看看具体的实现。AttributeAccessor
->AccessorHelper
->DoMakeAccessorHelperOne()里的MemberVariable
这是一条继承链,到了最下一层的时候所有的方法都已经定义,只是在不同的层次提供了不同的实现。
假设你已经搞明白Accessor
的继承链条了,也明白这个Accessor
到底支持什么操作,我们就进入了真正执行Set
的地方:
// DoMakeAccessorHelperOne()里的MemberVariable的方法
virtual bool DoSet (T *object, const V *v) const {
typename AccessorTrait<U>::Result tmp;
bool ok = v->GetAccessor (tmp);
if (!ok)
{
return false;
}
(object->*m_memberVariable) = tmp;
return true;
}
这里的V *v
就是myObj->SetAttribute ("MyIntAttribute", IntegerValue(3));
里的IntegerValue(3)
。要是没看懂,就去翻代码。这个结论是必须要搞懂的,不然就没有进行下去的必要了。
其实Accessor
的世界已经探索的差不多了,为了真正搞明白这个函数做了什么,我们先转向看看AttributeValue
。
AttributeValue
NS3的套路是什么?用宏和模板做代码生成。这个套路在AttributeValue
里也是一样的。自定义了一个AttributeValue
需要写一个宏,这个宏帮助我们做了大部分的事情。拿那个IntegerValue
说事儿:
ATTRIBUTE_VALUE_DEFINE_WITH_NAME (uint64_t, Uinteger);
// 在头文件里写这个宏,能够展开为如下的定义
class UintegerValue : public AttributeValue
{
public:
UintegerValue ();
UintegerValue (const uint64_t &value);
void Set (const uint64_t &value);
uint64_t Get (void) const;
template <typename T>
bool GetAccessor (T &value) const {
value = T (m_value);
return true;
}
virtual Ptr<AttributeValue> Copy (void) const;
virtual std::string
SerializeToString (Ptr<const AttributeChecker> checker) const;
virtual bool
DeserializeFromString (std::string value,
Ptr<const AttributeChecker> checker);
private:
uint64_t m_value;
}
// 上述定义的实现仰赖于对于cc文件里的实现,也是用宏
ATTRIBUTE_VALUE_IMPLEMENT_WITH_NAME (uint64_t,Uinteger);
// 展开后是这样的
UintegerValue::UintegerValue ()
: m_value () {}
UintegerValue::UintegerValue (const uint64_t &value)
: m_value (value) {}
void UintegerValue::Set (const uint64_t &v) {
m_value = v;
}
uint64_t UintegerValue::Get (void) const {
return m_value;
}
Ptr<AttributeValue>
UintegerValue::Copy (void) const {
return ns3::Create<UintegerValue> (*this);
}
std::string UintegerValue::SerializeToString
(Ptr<const AttributeChecker> checker) const {
std::ostringstream oss;
oss << m_value;
return oss.str ();
}
bool UintegerValue::DeserializeFromString
(std::string value, Ptr<const AttributeChecker> checker) {
std::istringstream iss;
iss.str (value);
iss >> m_value;
do {
if (!(iss.eof ()))
{
std::cerr << "aborted. cond="" << ""!(iss.eof ())"" << """"
脚本宝典总结
以上是脚本宝典为你收集整理的;全部内容,希望文章能够帮你解决;所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。