
二、NS3内核
本章参考书籍:《ns-3网络模拟器基础与应用》
1、对象模型
本节描述针对ns-3的对象设计。总体来说,ns-3中使用的设计模式包括经典的面向对象设计、接口与实现分离、非虚拟的公共接口设计模式、对象聚合以及引用计数内存管理模式。
(1)对象基类
ns-3 中提供了3个对象基类供用户继承。这 3个基类分别是:
Object
ObjectBase
SimpleRefCount
ns-3并不强迫每一个类都必须继承自这3个类,但是从这3个基类继承的类包含了ns-3提供的特有特性:
属性系统
对象聚合系统
智能指针和引用计数系统
从ObjectBase中继承的类包含上述前2个功能特性,但是不包含智能指针,从SimpleRefCount中继承的类只包含智能指针功能,只有从 Object类继承的子类才具备以上3种特性。
(2)内存管理
在C++程序中,内存管理一直是一个复杂问题,一旦操作不对就很有可能造成内存泄漏,而且如果用户使用了new操作符申请了内存,而忘记在变量无效时使用 delete 操作符释放内存,就容易导致内存不足的假象。因此,C++程序员在对new和delete的使用时要非常小心,这是程序员不愿意接受的。在ns-3中,使用引用计数设计模式来管理内存。
所有使用引用计数的对象都持有一个内部的计数器来判断对象是否可以安全地删除,释放己分配的内存空间。当一个接口获得一个对象指针时,该对象的引用计数通过调用函数Ref()来增加1,同样当用户不再使用该指针时,用户有义务调用Unref()函数来减小引用计数,当引用计数器减少为0时,该对象就被删除。
当用户通过构造函数或GetObject()函数来获取指针时,引用计数不增加。
当用户通过复制构造函数来获取一个指针时,引用计数增加。
所有的对象指针都必须通过Unref()来释放引用。
这里用户最关心的莫过于Ref()和Unref()这2个函数怎么调用,需要谁调用。ns-3提供了引用计数智能指针来调用这些函数,用户在编程时基本不需要考虑这些。
(3)引用计数指针(Ptr)
在编程时时时刻刻想着调用 Ref()和 Unref()是非常繁琐讨人厌的事,就好比使用new和delete一样,所以ns-3提供了引用计数指针 Ptr 类来处理上述函数调用。智能指针认为每一个基本类型提供一对函数 Ref()和 Unref()来增加和减小对象实例的引用计数。
智能指针的使用和C++里面指针的使用是相同的,可以被赋值为 空,也可以通过其他指针来复制等,熟悉指针的用户用起来非常顺 手。
如果你想要让一个指针指向一个对象,建议你使用 ns-3 提供的 CreateObject模版函数来创建类对象,并将其地址复制给指针以避免内存泄漏。ns-3提供的这个函数旨在为用户提供方便并减少代码量。
(4)CreateObject和Create
在C++中,对象的创建可以是动态的也可以使静态的,这些特征在ns-3中同样适用,不过ns-3还提供了一种创建方式,特别是针对包含引用计数的对象,就是通过模版函数CreateObject和Create来创建。
对于基类为Object的类创建对象的方法如下:
Ptr<MyObject> mo = CreateObject<MyObject>();
例如:
Ptr<WifiNetDevice> device = CreateObject<WifiNetDevice>();
在ns-3中尽量地使用CreateObject()来创建类对象,而不是在 C++中使用new操作符。只要这个类支持智能指针,强烈建议用户使用上述方法,这是对new操作符的一个封装,它能正确地处理智能指针引 用计数。
2、属性系统
在使用ns-3对网络进行模拟仿真时,要对以下2个方面进行设置。
对使用的网络构件进行连接组织和模拟拓扑。
对网络构件中实例化的模型所使用的参数值进行设置。
本节主要来学习第二项:ns-3中大量使用的数据是如何组织、记录以及用户如何来修改这些值。当然属性系统在跟踪和统计模块中也 起到了至关重要的作用。
在学习属性系统具体的细节之前大家需要先回顾类Object的一些 特征。
2.1、对象模型
在前面对象模型章节讲过ns-3本质上是一个基于C++对象的系统。这意味着新的C++类可以像标准C++一样被声明、定义以及继承等。 但是,同样ns-3提供了自己特有的定义类的方法,即大部分ns-3 类都是继承于Object类。这些对象有很多附加的属性,这些属性是为 了对系统进行组织和改进对象的内存管理而开发的。
Metadata系统将类名和meta-information进行连接,这些metainformation是与对象有关的,包括子类的基类、子类中可以访问的构造函数以及子类的属性集。
下面通过Node类从整体上对对象以及后面要学习到的属性系统进行一个简单的介绍。在头文件node.h中,类中声明了一个静态成员函数GetTypeId
如下:
Class Node : public Object
{
public:
Static TypeId GetTypeId(void):
...
}
然后在node.cc文件中对函数GetTypeId进行了定义,如下:
TypeId
Node::GetTypeId (void)
{
static TypeId tid = TypeId ("ns3::Node").SetParent<Object> ().AddConstructor<Node> ()
.AddAttribute ("DeviceList","The list of devices associated to this Node.", ObjectVectorValue (), MakeObjectVectorAccessor(&;Node::m_devices), MakeObjectVectorChecker<NetDevice> ())
.AddAttribute ("ApplicationList","The list of applications associated to this Node.", ObjectVectorValue (), MakeObjectVectorAccessor(&;Node::m_applications), MakeObjectVectorChecker<Application> ())
.AddAttribute("Id","The id(unique integer)of this Node.",TypeId::ATTR_GET, UintegerValue (0), MakeUintegerAccessor (&;Node::m_id), MakeUintegerChecker<uint32_t> ());
return tid;
}
ns-3中所有由Object类派生的类都包含一个叫TypeId的元数据类,该类用来记录关于类的元信息,以便在对象聚合以及构件管理中使用,TypeId类中涉及了用唯一的字符串来标识一个类、子类的基类以及子类中可以访问的构造函数。
这里可以把TypeId看作RTTI的一个扩展形式,RTTI(运行时类型识别)是标准C++提供的一种使程序能够使用基类的指针或引用来检查这些指针或引用所指对象的实际派生类型。
函数SetParent<Object>()
为声明该类的基类,方便在使用 GetObject()函数时安全地进行向上或向下类型转换。
函数 AddConstructor<Node>()
和抽象对象工厂机制可以方便用 户在不了解对象类型具体细节的情况下构建对象。
接下来的3个AddAttribute()
函数的调用目的是把一个给定的唯一 字符串和类的成员变量相关联。函数第1个参数是要绑定的字符串,第 2参数是对第1个参数的解释说明,第3个参数是要绑定的类成员变量必须转化的类型,第四个参数就是把类成员变量强制转化为第3个参数的类型,第4个参数是对第3个参数使用的成员变量是否合法的一个检查。
对象工厂机制简单说明一下。如果用户想要创建节点,可以使用CreateObject()函数,如下:
Ptr<Node> n = CreateObject<Node>();
也可以使用对象工厂机制来创建,如下:
ObjectFactory factory;
const st::string typeId = "ns3::Node";
factory.SetTypeId(typeId);
Ptr(Object) node = factory.Create<Object>();
2.2、属性系统
ns-3提出属性系统机制的目的就是为了管理和组织仿真中的内部对象。主要原因是用户在仿真过程中要不断地修改己经存在的仿真脚本,来跟踪、收集和研究仿真程序中变量或数据的变化,比如:
用户想要跟踪第一个接入点的无线接口上的分组。若用户对TCP拥塞窗口比较感兴趣,也就是说对拥塞窗口大小的跟踪。
用户想要获取并记录模拟上所有被使用的值。
类似地,用户可能想对模拟中的内部变量进行细致的访问,或者可能广泛地修改某个特定参数的初始值,以便涉及所有随后要创建的对象。用户可能知道在模拟配置中哪些变量是可以设置的,哪些是可以获许的。这不仅仅是为了命令行直接交互,还考虑到图像用户界面,该界面可能提供让用户在节点右击鼠标就能获取信息的功能,这些信息可能是一个层次性组织的参数列表,显示该节点上可以设置的参数以及构成节点的成员对象,还有帮助信息和每个参数的默认值。
ns-3给用户提供了可以访问系统深处的值的方法。
下面就来分析类DropTailQuenue,该类有一个叫做m_maxPackets的无符号整型成员变量,该成员控制队列的大小。
DropTailQueue 是一种在网络队列管理中常用的简单队列算法。在 DropTailQueue 中,当数据包到达并且队列已满时,新到达的数据包将被直接丢弃。这种队列管理方式不会检查数据包的种类或重要性,而是简单地按照先到先服务(FIFO)的原则处理数据包,一旦队列空间被占满,超出的数据包就会被丢弃。
class DropTailQueue : public Queue
{
public:
static typeId GetTypeId(void);
...
private:
std::queue<Ptr<Packet> > m_packets;
uint32_t m_maxPackets;
};
考虑用户可能对m_maxPackets的值想要做的事情:
为系统设置一个默认值,以便无论何时一个新的 DropTailQueue 被创建时,这个成员变量都被初始化为默认值。
对于一个己经实例化的对象,设置或获取该对象的值。
上述情况在标准C++中一般会提供Get()和Set()函数来实现。而在 ns-3中,通过属性系统中的TypeId类来实现。例如:
NS_OBJECT_ENSURE_REGISTERED (DropTailQueue);
TypeId DropTailQueue::GetTypeId (void)
{
static TypeId tid = TypeId ("ns3::DropTailQueue").SetParent<Queue>().AddConstructor<DropTailQueue>().AddAttribute ("MaxPackets","The maximum number of packets accepted by this DropTailQueue.", UintegerValue (100), MakeUintegerAccessor(&;DropTailQueue::m_maxPackets), MakeUintegerChecker<uint32_t> ());
return tid;
}
上述代码中方法AddAttribute()对m_maxPackets进行了一系列的处理。 将变量m_maxPackets绑定到一个字符串“MaxPackets”中。
提供默认值100。
提供为该值定义帮助的信息。
提供“checker”,可以用来设置所允许的范围。
该变量的值以及它的默认值在属性空间中是可以基于字符串“MaxPackets”访问的,下面将通过一个例子来说明用户是如何来操纵这些值的。
#include "ns3/log.h"
#include "ns3/command-line.h"
#include "ns3/ptr.h"
#include "ns3/config.h"
#include "ns3/uinteger.h"
#include "ns3/string.h"
#include "ns3/pointer.h"
#include "ns3/simulator.h"
#include "ns3/node.h"
#include "ns3/queue.h"
#include "ns3/drop-tail-queue.h"
#include "ns3/point-to-point-net-device.h"
using namespace ns3;
int main (int argc,char *argv[])
{
LogComponentEnable("AttributeValueSample",LOG_LEVEL_INFO);
Config::SetDefault("ns3::DropTailQueue::MaxPackets",StringValue ("80"));
Config::SetDefault("ns3::DropTailQueue::MaxPackets",UintegerValue (80));
CommandLine cmd;
cmd.Parse (argc,argv);
Ptr<Node> n0 = CreateObject<Node> ();
Ptr<PointToPointNetDevice> net0 = CreateObject<PointToPointNetDevice> ();
n0->AddDevice (net0);
Ptr<Queue> q = CreateObject<DropTailQueue> ();
net0->SetQueue (q);
PointerValue ptr;
net0->GetAttribute ("TxQueue",ptr);
Ptr<Queue> txQueue = ptr.Get<Queue> ();
Ptr<DropTailQueue> dtq = txQueue->GetObject<DropTailQueue>();
NS_ASSERT (dtq);
UintegerValue limit;
dtq->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("1.dtq limit: " << limit.Get () << "packets");
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("2.txQueue limit: " << limit.Get () << "packets");
txQueue->SetAttribute ("MaxPackets",UintegerValue(60));
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("3.txQueue limit changed: " << limit.Get() << " packets");
Config::Set("/NodeList/0/DeviceList/0/TxQueue/MaxPackets",UintegerValue (25));
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("4.txQueue limit changed through namespace: " << limit.Get () << " packets");
Config::Set("/NodeList/*/DeviceList/*/TxQueue/MaxPackets",UintegerValue (15));
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("5.txQueue limit changed through wildcarded namespace: " << limit.Get () << " packets");
Simulator::Destroy ();
}
(1)设置默认值,方法如下:
Config::SetDefault ("ns3::DropTailQueue::MaxPackets",StringValue ("80"));
Config::SetDefault ("ns3::DropTailQueue::MaxPackets",UintegerValue (80));
这两行代码的功能是为随后即将声明的 DropTailQueue 类的实例化设置默认值,换句话,假如随后生成了一个DropTailQueue对象 dtq,那么dtq的成员变量m_maxPackets的默认值为80。
正如上面两行代码所写的一样,使用SetDefaulet函数时,第二个参数可以是StringValue类型也可以是UintegerValue类型,具体使用什么类型用户可以自己诀定,没有什么实际的区别。
设置好默认的值以后用户就可以实例化自己的对象,如下:
Ptr<Node> n0 = CreateObject<Node> ();
Ptr<PointToPointNetDevice> net0 = CreateObject<PointToPointNetDevice> ();
n0->AddDevice (net0);
Ptr<Queue> q = CreateObject<DropTailQueue> ();
net0->SetQueue (q);
上述代码中,创建了一个唯一的节点(Node 0),并且在这个节 点上创建了一个唯一的PointToPointNetDevice网络设备(NetDevice 0),然后为其添加了一个尾部分组丢失队列。这里新创建对象的成员变量m_maxPackets的初始化值并不是系统一开始给定的值100,而是使用过SetDefaults函数修改过的值80。因此,可以得出结论,在每次创建一个对象时,都可以通过SetDefaults函数来修改默认的值。
上面大家学习了怎么在创建对象实例化之前修改默认的属性值,这里大家将继续学习怎么从己创建的对象中获取所想得到的属性值,或者在必要的情况下修改这些值。
(2)通过指针访问属性值
在本节的例子中首先创建一个指针变量,注意这里是创建一个指针变量而不是创建一个指针。
PointerValue ptr
然后为指针变量赋值:
net0->GetAttribute ("TxQueue",ptr);
获取队列:
Ptr<Queue> txQueue = ptr.Get<Queue> ();
下面通过GetObject函数来安全地把txQueue向下类型转化:
txQueue是父类,dtq是子类。把指向父类的指针转换成指向子类的了
Ptr<DropTailQueue> dtq = txQueue->GetObject<>(DropTailQueue);
下面可以通过输出数据来验证本程序是否将默认值100改成了80:
UintegerValue limit;
dtq->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("1.dtq limit: " << limit.Get () << "packets");
实际上,不进行向下类型转换一样可以得到 MaxPackets 值:
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("2.txQueue limit: " << limit.Get () << "packets");
如果用户想要在创建对象之后再改变MaxPackets的值该怎么做? 方法如下:
txQueue->SetAttribute ("MaxPackets",UintegerValue (60));
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("3.txQueue limit changed: " << limit.Get ()<< " packets");
(3)用户也可以通过基于命名空间的方式访问属性值
不同于上述通过指针访问属性值的方法,这里用户可以通过使用 =配置命名空间的方法来操作属性值。针对本例中使用的方法如下:
Config::Set("/NodeList/0/DeviceList/0/TxQueue/MaxPackets", UintegerValue(25));
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("4.txQueue limit changed through namespace: " << limit.Get () << " packets");
这样就将 MaxPackets 的值改为了 25。这里仅仅是修改了第一个节点的第一个网络设备对应的MaxPackets值。当然大家也可以修改其他节点的所有网络设备对应的MaxPackets值。如下:
Config::Set("/NodeList/*/DeviceList/*/TxQueue/MaxPackets",UintegerValue(15));
txQueue->GetAttribute ("MaxPackets",limit);
NS_LOG_INFO ("5.txQueue limit changed through wildcarded namespace:" << limit.Get () << " packets");
3、Tracing系统
你可以在编写代码时使用C++标准输出方式cout,但是从长远可用性来看,使用这种方式并不是非常让人满意的。因为随着自己在代码中增加更多的输出语句,处理大量的输出信息是非常复杂的。
在前面章节中大家学习了日志系统 Logging 也可以用于输出信息。
int main()
{
NS_LOG_UNCOND("Scratch Simulator");
return 0;
}
咋眼一看,使用日志系统Logging和使用C++标准输出没有太大的区别。但是通过Logging系统你可以控制输出的等级,这一特点在前面章节己经学习,这里不再赘述
无论是标准C++输出还是Logging系统输出,都仅仅适合非常小的程序。因为当程序不断增加时,打印语句和控制输出格式将变得非常艰难,即使能够得到想要的输出结果,那么分析这些多条复杂的信息也将变得没有可能。
所以,ns-3需要提供一种机制,这种机制允许用户进入系统内核来获取所需要的信息,前提是不改变内核代码和不用再次编译。更好的方式是,当用户感兴趣的信息发生改变时,系统通知用户对信息进行处理,而不是去深入到系统内核。
下面要讲到的Tacing系统机制就是ns-3提供的用来解决上述问题的一种方法。
3.1、综述
ns-3 Tracing系统大体可以分为3个部分:
Tracing Sources
Tracing Sinks
将 Tracing Sources 和 Tracing Sinks 关联一起的方法。
Tracing Sources是一个实体,它可以用来标记仿真中发生的时间,也可以提供一个访问底层数据的方法。例如,当一个网络设备或网卡收到一个网络分组时,Tracing Source 可以指示并提供一个途径将分组的内容传递给对该分组感兴趣的Tracing Sink。还有,Tracing Sources还可在感兴趣的状态发生变化时给出相应的指示。例如,TCP 网络协议模块中的拥塞窗口发生改变时,Tracing Sources会给出指示。
Trace Sources本身是起不到任何作用的,只有当它和一段有实际功能的代码相关联时才有意义,这段代码就是使用Trace Sources提供的信息来做相关事务。使用或者说消费Trace Sources提供信息的实体就称为Trace Sink。
换句话说,Trace Sources提供信息,而Trace Sink消费信息,它们2个可以比喻为生产者和消费者。
一个Trace Sources生产的信息可以没有Trace Sink消费,也可以一个或者多个Trace Sink消费,它们之间是一对多的夫系。这样大家就知道 了,单独使用Trace Sources或单独使用Trace Sink是没有任何意义的,而针对不同用户给出不同的Trace Sink代码来处理Trace Sources产生的信息时得出的结果也是不同的,也就是说用户可以根据自己的需求给出不同的Trace Sink以便得出不同的结果。
下面就通过一个简单的例子来说明Trace Sources和Trace Sink:
#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h""
#include "ns3/trace-source-accessor.h"
#include <iostream>
using namespace ns3;
首先要定义自己的类,该类的父类为Object,因此要引入头文件#include“ns3/object.h”,再次引入了 ns-3 自定义的无符号整型所声明的头文件#include“ns3/uinteger.h” 。
下 面 着 重 讲 解 traced-value.h 头文件,在这个头文件中引入了要跟踪数据的类型, 即 TracedValue。trace-source-accessor.h 这个头文件中包含了本程序要使用的能把自定义数据转换为Trace Sources的函数。
class MyObject : public Object
{
public:
static TypeId GetTypeId (void)
{
static TypeId tid = TypeId ("MyObject").SetParent(Object::GetTypeId ()).AddConstructor<MyObject> ().AddTraceSource ("MyInteger","An integer value to trace.", MakeTraceSourceAccessor (&;MyObject::m_myInt));
return tid;
}
MyObject () {}
TracedValue<uint32_t> m_myInt;
};
因为Tracing系统和属性系统有很大的关联,而属性系统和对象相关联,所以,每一个要追踪的数据都必须属于一个特定的类,这里定义这个类为 MyObject,而要追踪的数据为m_myInt。GetTypeId 这个函数在前面己经讲述过,这里要注意的是AddTraceSource函数,这个函数使得m_myInt成为一个Trace Sources。
void IntTrace (Int oldValue,Int newValue)
{
std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}
上述代码就是定义Trace Sink。
int main (int argc,char* argv[])
{
Ptr<MyObject> myObject = CreateObject<MyObject> ();
myObject->TraceConnectWithoutContext("MyInteger", MakeCallback(&;IntTrace));
myObject->m_myInt = 1234;
}
主函数中首先定义了一个类对象实例,这个实例中包含了一个 TraceSource。TraceConnectWithoutContext 这个函数将Trace Sources和 Trace Sink 相关联。
只要调用了这个函数,当Trace Sources数据m_myInt发生改变时,IntTrace函数才会被调用。最后一行代码可以被解释为把常量1234赋值给m_myInt,这时系统会识别这一行为,并将m_myInt赋值前和赋值后的2个值作为形参传递给身为Trace Sink的回调函数 IntTrace。
3.2、config path
在上一小节例子中学习的函数 TraceConnectWithoutContext 在实际编程中很少用到。通常使用被称作“config path”的子系统从系统中选取用户所要使用的Trace Sources。
下面的函数CourseChange就是要定义的Trace Sink,和定义普通函数没有太大的区别,只要在主函数前声明定义就行。这段代码大家应该比较了解,一个回调函数包含2个参数。
void CourseChange(string context,Ptr<const MobilityModel> model)
{
Vector position = model->GetPosition();
NS_LOG_UNCOND(context << " x = " << position.x << " y = " << position.y);
}
下面编写的代码就是使上面的 CourseChange(Trace Sink)和 CourseChange (Trace Source) 相关联的代码。下面代码放在 Simulator::Run();前面就好。
ostringstream oss;
oss << "/NodeList" << wifiStaNodes.Get(nWifi -1) -> GetId() <<
"/$ns3::MobilityModel/CoureChange";
Config::Connect(oss.str(),MakeCallback(&;CourseChange));
使用类Config的一个静态成员函数Connect将二者关联在一起。这个函数有2个参数,首先看第2个参数,这个参数功能是使函数CourseChange成为一个回调函数。
本小节主要针对第一个参数进行讲解,首先,这个参数是一个由各种字符组成的字符串。下面对该函数的参数——作为路径的字符串分析其代表的含义。
第一个“/”符号代表后面要紧跟的是命名空间,后面所跟的“/”符号可以像目录与子目录一样来理解。这里用到的命名空间为NodeList。而NodeList是一个仿真中使用的节点的一个列表,紧随其后的是这个列表的一个索引, 这里是通过调用Get函数来获取该节点,然后再通过GetId函数来得到该节点的索引。下一段字符串的第一个字符为“$”,当程序遇见这个符号时,就会调用函数GetObject来返回一个对象,这是因为大家在实际仿真中使用的对象聚合技术己经把许多对象全都集成在这个节点中。因为节点中集成了需要的对象,所以后面就要给出返回对象的类 型,这里要返回的对象类型为MobilityModel。而类MobilityModel有一个称为CoureChange 的属性,也就是我们要跟踪的Tracing Sources。
那么如何确定“Config path”是一个不可避免的问题,很简单,只要进入API文档,进入你需要的类,你就会发现一个标题Config Paths,之后就一目了然了。
static void
CwndTracer (Ptr<OutputStreamWrapper>stream, uint32_t oldval, uint32_t newval)
{
*stream->GetStream () << oldval << " " << newval << std::endl;
}
static void
TraceCwnd (std::string cwndTrFileName)
{
AsciiTraceHelper ascii;
if (cwndTrFileName.compare ("") == 0)
{
NS_LOG_DEBUG ("No trace file for cwnd provided");
return;
}
else
{
Ptr<OutputStreamWrapper> stream = ascii.CreateFileStream (cwndTrFileName.c_str ());
Config::ConnectWithoutContext ("/NodeList/1/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow",MakeBoundCallback (&CwndTracer, stream));
}
}
上述代码将要追踪第二个节点的第一个端口tcp拥塞窗口,有变化就会进入CwndTracer里面。
在 NS-3 模拟框架中,Config::ConnectWithoutContext 是一个非常关键的函数,用于建立追踪点(trace points)与回调函数之间的连接。这使得用户可以监控和响应模拟中特定事件的发生,如变量的改变或特定行为的执行。此函数的作用和具体实现细节如下:
功能解释
• 无上下文连接:ConnectWithoutContext 的主要特点是它连接追踪点和回调函数,而不包括任何上下文信息。这意味着回调函数不会接收到额外的参数描述事件发生的上下文,如事件发生的时间或地点。
• 效率考量:相比于带上下文的追踪(Connect 方法),使用 ConnectWithoutContext 可以略微提高性能,因为它避免了在每次事件触发时处理额外数据的开销。
参数说明
Config::ConnectWithoutContext 函数通常接受两个参数:
1. 追踪路径(String):这是一个字符串,指定了要连接的追踪点的路径。它描述了模拟中的一个特定变量或事件,通常涉及模拟对象的层级结构。
2. 回调函数(Callback):这是当追踪点被触发时执行的函数。NS-3 使用模板和绑定机制允许回调函数具有灵活的签名,但在使用 ConnectWithoutContext 时,回调通常不带参数或只带有与追踪点直接相关的参数。
为什么不使用fopen打开文件,而是通过asciiHelper呢?
在 NS-3 中,选择使用 AsciiTraceHelper 类而不是直接使用 C++ 的 fopen 来打开和操作文件,主要是出于以下几个原因:
1. 集成和便利性
AsciiTraceHelper 提供了一个与 NS-3 模拟环境高度集成的方式来创建和管理输出文件。这个帮助类封装了文件的打开、写入和关闭的所有细节,使得用户可以更专注于模拟的逻辑部分,而不必担心文件操作的具体实现。
2. 错误处理和安全性
使用 AsciiTraceHelper 创建文件流时,它内部会处理所有与文件相关的错误检查和异常。这意味着如果文件不能被创建或写入,NS-3 会提供错误消息并确保模拟不会因为文件操作错误而崩溃,这比直接使用 fopen 更安全。
3. 性能优化
AsciiTraceHelper 可能使用了特定的缓冲策略和写入优化,这对于大规模的模拟场景,特别是当需要记录大量的追踪数据时,可以显著提高性能。
4. 一致性和标准化
NS-3 提供了一套标准的追踪和日志系统,AsciiTraceHelper 是这套系统的一部分。使用这些工具可以保证所有的追踪文件都遵循同样的格式和标准,方便数据的后处理和分析。
5. 扩展性和维护
使用 AsciiTraceHelper 可以更容易地扩展文件输出功能。例如,如果未来需要支持不同的输出格式(如 XML 或 JSON),只需在 AsciiTraceHelper 中添加相应的支持即可,而不需要用户修改每一个使用 fopen 的地方。
6. 与模拟环境的整合
AsciiTraceHelper 能够直接与 NS-3 的仿真时间和其他仿真组件(如网络节点、协议栈等)整合,提供时间戳和节点信息等,这是直接使用标准文件 API 所无法提供的。