NS3-手册(官方2024.3.18版——1,2章)中文翻译

1 ORGANIZATION

本章描述了ns-3软件的总体组织结构和本手册的相应组织结构。

ns-3是一个离散事件网络模拟器,其仿真核心和模型是用C++语言实现的。ns-3是作为一个库构建的,它可以静态或动态地链接到定义仿真拓扑并启动模拟器的C++主程序。ns-3还将其几乎所有的API导出到Python,允许Python程序导入“ns3”模块,就像ns-3库在C++中通过可执行文件链接一样。

在这里插入图片描述

图1:ns-3的软件结构

ns-3的源代码大部分组织在src目录下,可以用图1来描述。我们将自下而上地工作;一般来说,在图中,模块只依赖于它们下面的模块。

我们首先描述了模拟器的核心;那些在所有协议、硬件和环境模型中通用的组件。仿真内核在src/core中实现。数据包是网络模拟器中的基本对象,在src/network中实现。这两个仿真模块本身旨在组成一个通用的仿真核心,可用于不同类型的网络,而不仅仅是基于internet的网络。

ns-3的上述模块独立于特定的网络和设备模型,这些将在本手册的后续部分中介绍。

除了上述ns-3核心之外,我们还在手册的开头部分介绍了另外两个模块,它们补充了基于C++的核心API。ns-3程序可以直接访问所有API,也可以使用所谓的helper API,该API为低级API调用提供了方便的包装或封装。ns-3程序可以写入两个api(或其组合)这一事实是模拟器的一个基本方面。在讨论与网络模拟相关的特定模型之前,我们还描述了ns-3中如何支持Python。

手册的其余部分主要关注模型的文档化和支持功能。下一部分重点介绍ns-3中的两个基本对象:NodeNetDevice。设计了两种特殊的NetDevice类型来支持网络仿真用例,下面将介绍仿真。下一章专门讨论与Internet相关的模型,包括Internet应用程序使用的套接字API。下一章将介绍应用程序,下一章将描述对仿真的额外支持,例如动画器和统计。

该项目维护了一个专门用于测试和验证ns-3代码的手册部分(参见ns-3手册中的测试部分)。

2 SIMULATOR

本章解释了ns-3模拟器的一些核心概念。

2.1 Events and Simulator

Ns-3是一个离散事件网络模拟器。从概念上讲,模拟器跟踪计划在指定模拟时间执行的许多事件。模拟器的工作是按时间顺序执行事件。一旦事件完成,模拟器将移动到下一个事件(或者如果事件队列中没有更多事件将退出)。例如,如果在模拟时间“100秒”调度的事件被执行,而下一个事件直到“200秒”才被调度,则模拟器将立即从100秒跳到200秒(模拟时间)以执行下一个事件。这就是“离散事件”模拟器的含义。

为了实现这一切,模拟器需要一些东西:

  1. 可以访问存储事件的事件队列并管理事件执行的模拟器对象
  2. 负责从队列中插入和删除事件的调度器
  3. 表示模拟时间的方法
  4. 事件本身

本手册的这一章描述了这些基本对象(模拟器,调度器,时间,事件)以及如何使用它们。

2.1.1 Event

事件表示改变模拟状态的东西,也就是说,在两个事件之间,模拟状态没有改变,事件可能会改变它(它也可能不会改变任何东西)。

请注意,理解事件的另一种方法是将其视为延迟的函数调用。有了适当的区别,离散事件模拟与“正常”程序没有太大的不同,在“正常”程序中,函数不会立即调用,而是标有“时间”,时间用于决定函数执行的顺序。

当然,这个时间是一个模拟时间,与“真实”时间有很大的不同。根据模拟的复杂性,模拟时间可以比“真实”时间前进得更快或更慢,但就像“真实”时间一样,只能前进。

事件的一个例子是数据包的接收,或者计时器的过期。

事件用以下方式表示:

  • 事件发生的时间
  • 处理该事件的函数的指针,
  • 处理该事件的函数的参数(如果有的话),
  • 其他内部结构。

事件是通过调用Simulator::Schedule来安排的,一旦安排好,就可以取消或删除它。移除意味着从调度器数据结构中移除,而取消将它们保留在数据结构中,但设置了一个布尔标志,该标志禁止在计划的时间调用绑定的事件函数。当模拟器调度事件时,将返回一个EventId。客户端可以使用此事件ID稍后取消或删除该事件;有关示例用法,参见示例程序src/core/examples/sample-simulator.{cc,py}。取消事件通常比删除事件的计算成本要低,但是取消的事件会消耗调度器数据结构中的更多内存,这可能会影响其性能。

事件由模拟器存储在调度器数据结构中。事件按模拟器时间的递增顺序处理,如果两个事件具有相同的调度时间,则具有最低唯一ID的事件(单调递增的计数器)将首先处理。换句话说,捆绑事件是按照FIFO顺序处理的。

请注意,并发事件(同时发生的事件)在实际系统中不大可能发生,但也不是说不可能。在ns-3中,并发事件很常见,原因有很多,其中之一就是时间表示。

在开发模型时,必须仔细考虑到这一点。

在事件执行过程中,模拟时间不会提前,即每个事件在零时间内执行。这是离散事件模拟中的一个常见假设,当在事件中执行的操作的计算复杂性可以忽略不计时也适用。当这个假设不成立时,就有必要安排第二个事件来模拟计算密集型任务的结束。

例如,假设有一台设备接收到一个数据包,并必须对其执行复杂的分析(例如,图像处理任务)。事件的顺序将是:

  • T(t) :数据包接收和处理,将结果保存在某处,并在(t+d)中安排一个事件,标志着数据处理的结束。

  • T(t+d) :检索数据,并在此基础上做其他事情。

因此,即使数据处理在执行第一个事件时确实返回了结果,数据也只有在第二个事件之后才被认为是有效的。

下面的图片可以帮助你澄清这个想法。

在这里插入图片描述

2.1.2 Simulator

模拟器类是访问事件调度工具的公共入口点。一旦安排了几个事件来启动模拟,用户就可以通过进入模拟器主循环(调用Simulator::Run)来开始执行它们。一旦主循环开始运行,它将按照从最早到最晚的顺序依次执行所有计划的事件,直到事件队列中没有剩余事件或调用了Simulator::Stop。

为了调度由模拟器主循环执行的事件,模拟器类提供了Simulator::Schedule*函数族。

1)处理带有不同签名的事件处理程序

这些函数被声明和实现为C++模板,以自动处理各种各样的C++事件处理程序签名。例如,要安排一个事件在未来10秒执行,并调用带有特定参数的C++方法或函数,您可以这样写:

void handler(int arg0, int arg1)
{
    std::cout << "handler called with argument arg0=" << arg0 << " and
        arg1=" << arg1 << std::endl;
}
Simulator::Schedule(Seconds(10), &handler, 10, 5);

它将输出:

handler called with argument arg0=10 and arg1=5

当然,这些C++模板也可以透明地处理C++对象上的成员方法:

要完成:成员方法示例

注:

  • ns-3 Schedule方法只有当函数和方法的参数少于5个时才会自动识别。如果你需要它们来支持更多的参数,请提交一个bug报告。
  • 熟悉术语“全绑定函子”的读者将会认识到模拟器::Schedule方法是一种自动构造此类对象的方法。

2)常用调度操作

模拟器API的设计使大多数事件的调度变得非常简单。它提供了三种变体(从最常用到最不常用排序):

  • Schedule方法,允许您通过提供当前模拟时间和目标事件到期日期之间的延迟来调度未来的事件。
  • ScheduleNow方法允许你为当前模拟时间安排一个事件:它们将在当前事件完成执行之后执行,但在下一个事件的模拟时间改变之前执行。
  • ScheduleDestroy方法,它允许你在模拟器的关闭过程中挂钩,以清理模拟资源:每个’ destroy '事件被执行时,用户调用模拟器::destroy方法。

3)维护模拟上下文

有两种基本的方式来安排事件,有和没有上下文。这是什么意思?

Simulator::Schedule(Time const &time, MEM mem_ptr, OBJ obj);

vs.

Simulator::ScheduleWithContext(uint32_t context, Time const &time, MEM mem_ptr, OBJ obj);

投入时间和精力开发或使用重要模拟模型的读者将知道ns-3日志框架在调试简单和复杂模拟方面的价值。这个日志框架提供的一个重要特性是自动显示与“当前”运行事件相关的网络节点id。

当前正在执行的网络节点的节点id实际上由模拟器类跟踪。它可以通过模拟器::GetContext方法访问,该方法返回关联并存储在当前执行事件中的“context”(一个32位整数)。在一些罕见的情况下,当事件不与特定的网络节点相关联时,它的“上下文”被设置为0xffffffff。

要将上下文关联到每个事件,Schedule和scheduleow方法会自动重用当前执行事件的上下文,作为计划稍后执行的事件的上下文。

在某些情况下,尤其是在模拟数据包从一个节点到另一个节点的传输时,这种行为是不可取的,因为接收事件的预期上下文是接收节点的上下文,而不是发送节点的上下文。为了避免这个问题,模拟器类提供了一个特定的调度方法:ScheduleWithContext,它允许显式地提供与接收事件相关联的接收节点的节点id。

XXX:代码示例

在一些非常罕见的情况下,开发人员可能需要修改或理解如何将第一个事件的上下文(节点id)设置为其关联节点的上下文。这是由NodeList类完成的:每当创建新节点时,NodeList类使用ScheduleWithContext为该节点调度“初始化”事件。因此,“initialize”事件在执行时将上下文设置为节点id的上下文,并且可以使用各种正常的Schedule方法。它调用Node::Initialize方法,该方法通过调用与节点关联的每个对象的DoInitialize方法来传播“Initialize”事件。在这些对象中覆盖的DoInitialize方法(最明显的是在Application基类中)将调度一些事件(最明显的是Application::StartApplication),这些事件将依次调度流量生成事件,这些事件将依次调度网络级事件。

注:

  • 用户需要小心地通过在其成员对象上显式调用Initialize来跨对象传播DoInitialize方法
  • 与每个ScheduleWithContext方法相关联的上下文id除了日志记录之外还有其他用途:ns-3的一个实验分支使用它在使用多线程的多核系统上执行并行模拟。

Simulator::*函数不知道上下文是什么:它们只是确保当使用::GetContext执行相应的事件时,使用ScheduleWithContext指定的上下文是可用的。

由在Simulator::*之上实现的模型来解释上下文值。在ns-3中,网络模型将上下文解释为生成事件的节点的节点id。这就是为什么在ns3::Channel子类中调用ScheduleWithContext很重要,因为我们正在从节点i生成一个事件到节点j,我们希望确保将在节点j上运行的事件具有正确的上下文。

Available Simulator Engines

Ns-3提供两种不同类型的基本模拟器引擎来管理事件执行。这些是从抽象基类SimulatorImpl派生的:

  • DefaultSimulatorImpl 这是一个经典的顺序离散事件模拟器引擎,它使用单个线程执行。这个引擎尽可能快地执行事件。
  • DistributedSimulatorImpl 这是一个经典的YAWNS分布式(“并行”)模拟器引擎。通过适当地标记和实例化您的模型组件,这个引擎将在许多计算过程中并行地执行模型,但是以时间同步的方式,就好像模型是顺序执行的一样。这样做的两个优点是可以更快地执行模型,并且可以执行一个计算节点无法容纳的大模型。这个引擎也试图尽可能快地执行。
  • NullMessageSimulatorImpl 这实现了Chandy- Misra-Bryant (CMB)空消息算法的一种变体,用于并行仿真。像DistributedSimulatorImpl一样,这需要适当地标记和实例化模型组件。此引擎试图尽可能快地执行事件。

你可以通过设置一个全局变量来选择使用哪个模拟器引擎,例如:

GlobalValue::Bind("SimulatorImplementationType",
				   StringValue("ns3::DistributedSimulatorImpl"));

或者使用命令行参数

$ ./ns3 run "... --SimulatorImplementationType=ns3::DistributedSimulatorImpl"

除了基本的模拟器引擎之外,还有一个通用的工具用于构建“适配器”,它为核心的SimulatorImpl引擎之一提供小的行为修改。适配器基类是SimulatorAdapter,它本身派生自SimulatorImpl。SimulatorAdapter使用PIMPL(pointer to implementation,实现指针)习惯将所有调用转发到配置的基本模拟器引擎。这使得通过覆盖所需的特定模拟器调用来提供小型定制变得容易,并允许SimulatorAdapter处理其余部分。

目前使用适配器的地方很少:

  • RealtimeSimulatorImpl 这个适配器试图通过调整时钟演进的速度来实时执行。这种节奏是“尽最大努力”,这意味着实际的事件执行可能不会完全同步,但会接近同步。这个引擎通常只与DefaultSimulatorImpl一起使用,但它可以用来保持分布式模拟与实时同步。请参阅实时章节。
  • VisualSimulatorImpl 此适配器启动正在运行的模拟的实时可视化,显示网络图和遍历链接的每个数据包。
  • LocalTimeSimulatorImpl 此适配器允许将有噪声的本地时钟附加到节点,然后根据本地有噪声的时钟(而不是相对于真实的模拟器时间)调度事件。

除了SimulatorAdapter的PIMPL习惯用法之外,还有一个特殊的按事件用户化hook:

SimulatorImpl::PreEventHook( const EventId & id)

可以使用它在实际执行下一个事件之前执行任何内务操作。

核心引擎和适配器之间的区别如下:只能有一个核心引擎在运行,而可以有几个适配器连接在一起,每个适配器提供基本引擎执行的变体。例如,可以将嘈杂的本地时钟与实时适配器一起使用。

可以通过上述相同的两种方法在DefaultSimulatorImpl之上添加单个适配器:绑定“SimulatorImplementationType”全局值或使用命令行参数。要连接多个适配器,必须使用不同的方法;参见SimulatorAdapter::AddAdapter() API文档

模拟器引擎类型可以设置一次,但必须在第一次调用Simulator () API之前设置。在实践中,由于一些模型在构造时必须安排它们的启动事件,这意味着通常您应该在实例化任何其他模型组件之前设置引擎类型。

引擎类型可以在Simulator::Destroy()之后,但在对模拟器API进行任何额外调用之前更改,例如在单个ns-3调用中执行多个运行时。

2.1.3 Time

Ns-3内部将模拟时间和持续时间表示为64位带符号整数(符号位用于负持续时间)。时间值是根据习惯的SI单位中的“分辨率”单位来解释的:fs, ps, ns, us, ms, s, min, h, d, y。该单位定义了最小时间值。它可以在调用Simulator::Run()之前更改一次。它不与64位时间值本身一起存储。

时间可以从所有标准数字类型(使用配置的默认单位)或显式单位(如Time MicroSeconds (uint64_t value))构造。时间可以比较,测试符号或等于零,四舍五入到给定的单位,转换为特定单位的标准数字类型。支持所有基本的算术运算(加、减、乘、除一个标量(数值))。Times可以写入/读取IO流。在写入的情况下,很容易选择输出单元,不同于分辨率单元。

2.1.4 Scheduler

Scheduler(调度程序)类的主要工作是维护未来事件的优先级队列。调度程序可以用一个全局变量来设置,类似于选择SimulatorImpl:

GlobalValue::Bind("SchedulerType",
				   StringValue("ns3::DistributedSimulatorImpl"));

调度程序可以在任何时候通过Simulator::SetScheduler()进行更改。默认的调度程序是MapScheduler,它使用std::map<>按时间顺序存储事件。

由于事件分布因模型而异,因此没有一个优先级队列的最佳策略,因此ns-3有几个具有不同权衡的选项。示例utils/bench-scheduler.c可用于测试用户提供的事件分布的性能。对于适度的执行时间(比如少于一个小时),优先级队列的选择通常不重要;将构建类型配置为优化对于减少执行时间更为重要。

下表列出了可用的调度器类型,以及它们在Insert()和RemoveNext()上的时间和空间复杂性的摘要。有关其他API调用复杂性的详细信息,请参阅各个Scheduler API页面

Scheduler 类型复杂度
SchedulerImpl 类型方法时间空间
Insert()RemoveNext()OverheadPer Event
CalendarScheduler<std::list> [] 常数常数24字节16字节
HeapSchedulerstd::vector上的堆对数对数24字节0
ListSchedulerstd::list线性常数24字节16字节
MapSchedulerstd::map 对数常数40字节32字节
PriorityQueueSchedulerstd::priority_queue<,std::vector>对数对数24字节0

2.2 Callbacks

一些ns-3的新用户不熟悉代码中广泛使用的编程习惯用法:ns-3回调。本章提供了回调的一些动机,如何使用它的指导,以及它的实现细节

2.2.1 Callbacks Motivation

假设您有两个仿真模型A和B,并且您希望它们在仿真期间在它们之间传递信息。一种方法是让A和B明确了解对方,这样它们就可以调用对方的方法:

class A {
public:
	void ReceiveInput( /* 参数 */ );
	...
}

并在另一个源文件中:

class B {
public:
	void DoSomething();
	...
private:
	A* a_instance; // 指针指向 A
}
void
B::DoSomething()
{
	// 告诉 a_instance 发生了什么事情
	a_instance->ReceiveInput( /* 参数 */ );
	...
}

这当然有效,但它的缺点是,它引入了对a和B的依赖,以便在编译时了解对方(这使得在模拟器中拥有独立的编译单元变得更加困难),并且不通用;如果在以后的使用场景中,B需要与一个完全不同的C对象通信,则需要更改B的源代码以添加c_instance等等。很容易看出,这是一种暴力的通信机制,可能会导致模型中的编程问题。

这并不是说,如果对象之间存在硬依赖关系,它们就不应该知道彼此,而是说,如果模型的交互在编译时受到的约束较少,那么模型通常可以变得更加灵活。

对于网络仿真研究来说,这不是一个抽象的问题,相反,当研究人员想要扩展或修改系统以做不同的事情时(正如他们在研究中倾向于做的那样),它已经成为以前模拟器问题的根源。例如,考虑想要在TCP和IP之间添加IPsec安全协议子层的用户:

如果模拟器做出假设并硬编码到代码中,即IP总是与上面的传输协议通信,则用户可能被迫入侵系统以获得所需的互连。这显然不是设计通用模拟器的最佳方式。

2.2.2 Callbacks Background


注意:熟悉编程回调的读者可以跳过本教程部分。

允许解决上述问题的基本机制被称为回调。最终目标是允许一段代码调用函数(或C++中的方法),而不需要任何特定的模块间依赖关系。

这最终意味着您需要某种间接方式——您将被调用函数的地址视为一个变量。这个变量称为指向函数的变量。函数和指针到函数之间的关系与对象和指针到对象之间的关系没有什么不同。

在C语言中,指针到函数的典型示例是pointer-to-function-returning-integer(指针到函数返回整数,PFI)。对于接受一个int形参的PFI,可以这样声明:

int (*pfi)(int arg) = 0;

从这里得到的是一个名为pfi的变量,它被初始化为值0。如果要将该指针初始化为有意义的对象,则必须有一个具有匹配签名的函数。在这种情况下:

int MyFunction(int arg) {}

如果你有这个目标,你可以初始化变量,使其指向你的函数,如下所示:

pfi = MyFunction;

然后,您可以使用更具暗示性的调用形式间接调用MyFunction:

int result = (*pfi)(1234);

这很有启发性,因为它看起来就像对函数指针解引用一样,就像对任何指针解引用一样。然而,通常情况下,人们利用编译器知道发生了什么的事实,只使用更短的形式:

int result = pfi(1234);

注意,函数指针遵循值语义,因此可以像传递其他值一样传递它。通常,当你使用异步接口时,你会传递一些像这样的实体给一个函数,它会执行一个动作并回调,让你知道它完成了。它通过遵循间接调用并执行所提供的函数来回调。

在C++中,对象的复杂性增加了。与上述PFI的类比意味着您有一个指向返回int的成员函数的指针 (a pointer to a
member function returning an int,PMI),而不是指向返回int 的函数的指针(a pointer to function returning an int,PFI)。

提供间接的变量的声明看起来只有轻微的不同:

int (MyClass::*pmi)(int arg) = 0;

它声明了一个名为pmi的变量,就像前面的例子声明了一个名为pfi的变量一样。由于将调用特定类的实例的方法,因此必须在类中声明该方法:

class MyClass {
public:
	int MyMethod(int arg);
};

给出这个类声明,然后可以像这样初始化该变量:

pmi = &MyClass::MyMethod;

这将实现该方法的代码的地址分配给该变量,从而完成间接传递。为了调用一个方法,代码需要一个this指针。反过来,这意味着必须有一个MyClass的对象来引用。一个简单的例子就是间接调用一个方法(想想虚函数):

int (MyClass::*pmi)(int arg) = 0; 	// 声明一个PMI
pmi = &MyClass::MyMethod; 			// 指向实现代码
MyClass myClass; 					// 需要这个类的实例
(myClass.*pmi)(1234); 				// 使用对象ptr调用该方法

就像在C示例中一样,您可以在对另一个模块的异步调用中使用此方法,该模块将使用方法和对象指针进行回调。可以考虑的简单扩展是传递指向对象和PMI变量的指针。这个模块只会做:

(*objectPtr.*pmi)(1234);

对所需对象执行回调.

有人可能会问,这有什么意义?被调用的模块必须理解调用对象的具体类型,以便正确地进行回调。为什么不接受传递正确类型的对象指针并在代码中执行object->Method(1234),而是回调?这正是上面所描述的问题。需要的是一种将调用函数与被调用类完全解耦的方法。这个需求导致了函子(Functor)的发展。

函子是20世纪60年代发明的闭包的产物。它基本上只是一个打包的函数调用,可能带有一些状态。

函子有两个部分,特定部分和泛型部分,它们通过继承相互关联。调用代码(执行回调的代码)将执行泛型函子的泛型重载operator(),以导致回调被调用。被调用的代码(希望被回调的代码)必须提供operator()的专门实现,该实现执行导致上述紧密耦合问题的特定于类的工作。

创建了特定的函子及其重载的operator()后,被调用的代码将专用代码提供给将执行回调的模块(调用代码)。

调用代码将接受泛型函子作为形参,因此在函数调用中进行隐式强制类型转换,将特定函子转换为泛型函子。这意味着调用模块只需要理解泛型函子类型。它与调用代码完全解耦。

创建特定函子所需的信息是对象指针和指向方法的指针地址。

需要做的事情的本质是系统声明了函子的泛型部分:

template <typename T>
class Functor
{
public:
	virtual int operator()(T arg) = 0;
};

调用者定义了函子的特定部分,该部分实际上只是用来实现特定的operator()方法:

template <typename T, typename ARG>
class SpecificFunctor : public Functor<ARG>
{
public:
	SpecificFunctor(T* p, int (T::*_pmi)(ARG arg))
    {
        m_p = p;
        m_pmi = _pmi;
    }
    virtual int operator()(ARG arg)
    {
        (*m_p.*m_pmi)(arg);
    }
private:
	int (T::*m_pmi)(ARG arg);
	T* m_p;
};

下面是一个用法的例子:

class A
{
public:
A(int a0) : a(a0) {}
int Hello(int b0)
{
	std::cout << "Hello from A, a = " << a << " b0 = " << b0 << std::endl;
}
int a;
};
int main()
{
    A a(10);
    SpecificFunctor<A, int> sf(&a, &A::Hello);
    sf(5);
}

注:以上代码并非真正的ns-3代码。它是简单的示例代码,仅用于说明所涉及的概念并帮助您更多地理解系统。不要期望在ns-3树的任何地方找到这段代码。

注意,在上面的类中定义了两个变量。m_p变量是对象指针,m_pmi是包含要执行的函数地址的变量。

注意,当调用operator()时,它会使用C++ PMI语法调用对象指针提供的方法。

要使用它,可以声明一些以泛型函子作为参数的模型代码:

void LibraryFunction(Functor functor);

与模型对话的代码将构建一个特定的函子并将其传递给LibraryFunction:

MyClass myClass;
SpecificFunctor<MyClass, int> functor(&myclass, MyClass::MyMethod);

LibraryFunction完成后,它使用operator()对传递给它的泛型函子执行回调,并在此特殊情况下提供整型参数:

void
LibraryFunction(Functor functor)
{
    // 执行库函数
    functor(1234);
}

注意,LibraryFunction与客户端的特定类型完全解耦。这种连接是通过Functor多态性实现的

ns-3中的回调API使用函子机制实现了面向对象的回调。这个回调API基于C++模板,是类型安全的;也就是说,它执行静态类型检查来强制调用方和被调用方之间的签名兼容。因此,与传统的函数指针相比,使用它更具有类型安全性,但语法乍一看可能会让人觉得有些勉强。本节旨在引导您了解Callback系统,以便您可以在ns-3中轻松使用它

2.2.3 Using the Callback API

Callback API相当小,只提供两个服务:

  1. 回调类型声明:一种声明具有给定签名的回调类型的方法;
  2. 回调实例化:一种实例化模板生成的转发回调的方法,它可以将任何调用转发到另一个C++类成员方法或C++函数。

最好通过基于samples/main-callback.cc的示例来观察这一点。

Using the Callback API with static functions

考虑一个函数:

static double
CbOne(double a, double b)
{
    std::cout << "invoke cbOne a=" << a << ", b=" << b << std::endl;
    return a;
}

考虑下面的主程序片段:

int main(int argc, char *argv[])
{
    // return type: double
    // first arg type: double
    // second arg type: double
    Callback<double, double, double> one;
}

这是一个c风格回调的例子,它不包括或不需要this指针。函数模板Callback实质上是对包含指向函数的指针的变量的声明。在上面的例子中,我们显式地展示了一个指向函数的指针,该函数返回一个整数并接受单个整数作为形参。Callback模板函数是该函数的泛型版本——它用于声明回调的类型。


注意:不熟悉C++模板的读者可以参考 http://www.cplusplus.com/doc/tutorial/templates/

Callback模板需要一个强制参数(要分配给这个回调的函数的返回类型)和最多五个可选参数,每个可选参数指定参数的类型(如果您的特定回调函数有超过五个参数,那么可以通过扩展回调实现来处理)。

所以在上面的例子中,我们声明了一个名为“one”的回调函数,它最终将保存一个函数指针。它将保存的函数的签名必须返回double,并且必须支持两个double参数。如果试图传递签名与声明的回调不匹配的函数,则会发生编译错误。此外,如果试图将不兼容的回调分配给回调,编译将成功,但将引发运行时NS_FATAL_ERROR。示例程序src/core/examples/main-callback.ccmain()程序的末尾演示了这两种错误情况。

现在,我们需要将这个回调实例和实际的目标函数(CbOne)联系在一起。注意,上面的CbOne与回调函数具有相同的函数签名类型——这一点很重要。我们可以将任何类型正确的函数传递给这个回调。让我们更仔细地看看这个:

static   double CbOne(double a, double b) {}
            ^ 			^ 			^
            | 			| 			|
            | 			| 			|
Callback<double,      double,   double> one;

只有当函数和回调函数具有匹配的签名时,才能将它们绑定到回调函数。第一个模板参数是返回类型,其他模板参数是函数签名的参数类型。

现在,让我们将回调函数“one”绑定到与其签名匹配的函数:

// 构建指向cbOne函数的回调实例
one = MakeCallback(&CbOne);

这个对MakeCallback的调用实际上是创建上面提到的一个专用函子。使用Callback模板函数声明的变量将扮演泛型函子的角色。赋值one = MakeCallback(&CbOne)是强制类型转换,它将被调用方已知的专门化函子转换为调用方已知的泛型函子。

然后,在程序的后面,如果需要回调,可以这样使用:

NS_ASSERT(!one.IsNull());

// 通过回调实例调用cbOne函数
double retOne;
retOne = one(10.0, 20.0);

IsNull()的检查确保回调不是空的——在这个回调的后面有一个函数要调用。然后,one()执行泛型operator(),它实际上重载了operator()的特定实现,并返回与直接调用CbOne()相同的结果。

Using the Callback API with member functions

通常,您不会调用静态函数,而是调用对象的公共成员函数。在这种情况下,MakeCallback函数需要一个额外的参数,以告诉系统应该在哪个对象上调用该函数。考虑这个例子,同样来自main-callback.cc:

class MyCb {
public:
    int CbTwo(double a) {
        std::cout << "invoke cbTwo a=" << a << std::endl;
        return -5;
    }
};
int main()
{
    ...
    // return type: int
    // first arg type: double
    Callback<int, double> two;
    MyCb cb;
    // 构建指向MyCb::cbTwo的回调实例
    two = MakeCallback(&MyCb::CbTwo, &cb);
    ...
}

这里,我们向MakeCallback<>函数传递了一个额外的对象指针。回想一下上面的background小节,当Operator()在对象上执行时,它将使用指向成员的指针语法:

virtual int operator()(ARG arg)
{
    (*m_p.*m_pmi)(arg);
}

因此,在构造特定的函子时,需要提供两个变量(m_pm_pmi)。这里是这样的:

two = MakeCallback(&MyCb::CbTwo, &cb);

在这种情况下,当调用two()时:

int result = two(1.0);

将导致对&cb所指向对象的CbTwo成员函数(方法)的调用。

Building Null Callbacks

回调可能为空;因此,在使用它们之前进行检查可能是明智的。null回调有一个特殊的构造,它比简单地传递“0”作为参数更可取;它是MakeNullCallback<>构造:

two = MakeNullCallback<int, double>();
NS_ASSERT(two.IsNull());

调用空回调就像调用空函数指针:它将在运行时崩溃。

2.2.4 Bound Callbacks

对函子概念的一个非常有用的扩展是绑定回调。前面提到过,闭包最初是打包起来供以后执行的函数调用。注意,在上面所有的Callback描述中,没有办法打包任何参数供以后使用——当Callback通过operator()调用时。所有参数都由调用函数提供。

如果希望允许客户端函数(提供回调的函数)提供一些参数,该怎么办?Alexandrescu将允许客户端指定其中一个参数的过程称为“绑定”。operator()的一个参数已被客户端绑定(固定)。

我们的一些pcap跟踪代码提供了一个很好的示例。每当接收到数据包时,都需要调用一个函数。该函数调用一个对象,该对象实际以pcap文件格式将数据包写入磁盘。其中一个函数的签名将是:

static void DefaultSink(Ptr<PcapFileWrapper> file, Ptr<const Packet> p);

static关键字意味着这是一个不需要this指针的静态函数,因此它将使用c风格的回调。我们不希望调用代码必须知道数据包之外的任何信息。在调用代码中,我们想要的只是这样一个调用:

m_promiscSnifferTrace(m_currentPkt);

我们要做的是在创建Ptr文件时将其绑定到Ptr<PcapFileWriter> file特定的回调实现,并安排callback的operator()免费提供该参数。

我们为此提供了MakeBoundCallback模板函数。它接受与MakeCallback模板函数相同的参数,但也接受要绑定的参数。在上面的例子中:

MakeBoundCallback(&DefaultSink, file);

将创建一个特定的回调实现,该实现知道添加额外的边界参数。从概念上讲,它用一个或多个有界参数扩展上面描述的特定函子:

template <typename T, typename ARG, typename BOUND_ARG>
class SpecificFunctor : public Functor
{
    public:
        SpecificFunctor(T* p, int (T::*_pmi)(ARG arg), BOUND_ARG boundArg)
        {
        m_p = p;
        m_pmi = pmi;
        m_boundArg = boundArg;
        }
        virtual int operator()(ARG arg)
        {
        (*m_p.*m_pmi)(m_boundArg, arg);
        }
    private:
        void (T::*m_pmi)(ARG arg);
        T* m_p;
        BOUND_ARG m_boundArg;
};

您可以看到,当创建特定的函函数时,绑定参数保存在函函数/回调对象本身中。当使用单个参数调用operator()时,如下所示:

m_promiscSnifferTrace(m_currentPkt);

operator()的实现将绑定形参添加到实际的函数调用中:

(*m_p.*m_pmi)(m_boundArg, arg);

也可以绑定两个或三个参数。假设我们有一个带有签名的函数:

static void NotifyEvent(Ptr<A> a, Ptr<B> b, MyEventType e);

可以创建绑定回调绑定前两个参数:

MakeBoundCallback(&NotifyEvent, a1, b1);

假设a1和b1分别是A和B类型的对象。类似地,对于三个参数,可以使用带有签名的函数:

static void NotifyEvent(Ptr<A> a, Ptr<B> b, MyEventType e);

在done with中绑定三个参数:

MakeBoundCallback(&NotifyEvent, a1, b1, c1);

再次假设a1 b1和c1分别是A B C类型的对象。

这种绑定可以用于仿真中对象之间的信息交换;具体来说,绑定回调可以用作跟踪回调,这将在下一节中描述。

2.2.5 Traced Callbacks

节占位符(官方文档也没有写)

2.2.6 Callback locations in ns-3

在ns-3中,哪些地方经常使用回调?下面是一些典型用户更容易看到的API:

  • 套接字API
  • 第二层/第三层API
  • 跟踪子系统
  • IP和路由子系统之间的API

2.2.7 Implementation details

上面的代码片段很简单,只是为了说明机制本身。实际的Callback代码非常复杂,模板非常密集,不需要对代码有深入的理解。如果有兴趣,专家用户可能会发现以下内容很有用。

代码最初是基于http://www.codeproject.com/cpp/TTLFunction.asp中描述的技术编写的。它随后被重写,以遵循 Modern C++ Design, Generic Programming
and Design Patterns Applied, Alexandrescu, chapter 5, Generalized Functors.

这段代码使用:

  • 默认模板参数,当参数的数量小于支持的最大数量时,用户不必指定空参数

  • pimpl习惯用法:Callback类通过值传递,并将工作的关键委托给它的pimpl指针。

  • 从CallbackImpl派生的两个pimpl实现FunctorCallbackImpl可以用于任何函数类型,而MemPtrCallbackImpl可以用于指向成员函数的指针。

  • 一个引用列表实现来实现回调的值语义。

这段代码与Alexandrescu实现最明显的不同之处是,它没有使用类型列表来指定和传递回调参数的类型。当然,它也不使用复制销毁语义,并且依赖于引用列表而不是autoPtr来保存指针。

2.3 Object model

ns-3基本上是一个C++对象系统。根据C++规则,对象可以像往常一样声明和实例化。ns-3还为传统的C++对象添加了一些特性,如下所述,以提供更强大的功能和特性。本节旨在向读者介绍ns-3对象模型。

本节描述ns-3对象的C++类设计。简而言之,使用的几种设计模式包括经典的面向对象设计(多态接口和实现)、接口和实现的分离、非虚拟公共接口设计模式、对象聚合功能以及用于内存管理的引用计数。熟悉组件模型(如COM或Bonobo)的人会在ns-3对象聚合模型中识别设计元素,尽管ns-3设计并不严格遵循这两种模型。

2.3.1 Object-oriented behavior

一般来说,C++对象提供了常见的面向对象功能(抽象、封装、继承和多态性),这些功能是经典面向对象设计的一部分。ns-3对象使用这些属性;例如:

class Address
{
public:
    Address();
    Address(uint8_t type, const uint8_t *buffer, uint8_t len);
    Address(const Address & address);
    Address &operator=(const Address &address);
    ...
private:
    uint8_t m_type;
    uint8_t m_len;
    ...
};

2.3.2 Object base classes

ns-3中使用了三个特殊的基类。从这些基类继承的类可以实例化具有特殊属性的对象。这些基类是:

  • Object
  • ObjectBase
  • SimpleRefCount

ns-3对象不需要从这些类继承,但是那些继承了特殊属性的对象。从类Object派生的类获得以下属性。

  • ns-3类型和属性系统(参见[配置和属性](#2.4 Configuration and Attributes))
  • 对象聚合系统
  • 智能指针引用计数系统(类Ptr)

从类ObjectBase派生的类获得上面的前两个属性,但不获得智能指针。从类SimpleRefCount派生的类:只获得智能指针引用计数系统。

在实践中,类Object是ns-3开发人员最常遇到的上述三个类的变体。

2.3.3 Memory management and class Ptr

C++程序中的内存管理是一个复杂的过程,并且经常不正确或不一致地完成。我们确定了如下所述的引用计数设计。

所有使用引用计数的对象都维护一个内部引用计数,以确定对象何时可以安全地删除自己。每次获得指向接口的指针时,通过调用Ref()增加对象的引用计数。

指针的使用者有义务在指针完成后显式地Unref()指针。当引用计数降为0时,该对象被删除。

  • 当客户端代码通过对象创建或GetObject从对象本身获取指针时,它不必增加引用计数。

  • 当客户端代码从另一个源获取指针(例如,复制指针)时,它必须调用Ref()来增加引用计数。

  • 对象指针的所有用户必须调用Unref()来释放引用。

使用下面描述的引用计数智能指针类,在一定程度上减轻了调用Unref()的负担。

使用低级API的用户如果希望使用operator new显式地在堆上分配非引用计数对象,则需要负责删除此类对象。

Reference counting smart pointer (Ptr)

一直调用Ref()Unref()会很麻烦,所以ns-3提供了一个类似于Boost::intrusive_ptr的智能指针类Ptr。这个智能指针类假设底层类型提供一对RefUnref方法,这两个方法可以增加和减少对象实例的内部重新计数。

这个实现允许你像操作普通指针一样操作智能指针:你可以将它与零进行比较,将它与其他指针进行比较,将它赋值为零,等等。

可以使用GetPointer()PeekPointer()方法从这个智能指针中提取原始指针。

如果你想将新对象存储到智能指针中,我们建议你使用CreateObject模板函数来创建对象并将其存储在智能指针中,以避免内存泄漏。这些函数都是非常方便实用的工具,它们的目标只是为了帮助你减少一些输入。

2.3.4 CreateObject and Create

C++中的对象可以是静态、动态或自动创建的。这也适用于ns-3,但是系统中的一些对象有一些额外的框架可用。具体来说,引用计数对象通常使用模板化的Create或CreateObject方法分配,如下所示。

对于从Object类派生的对象:

Ptr<WifiNetDevice> device = CreateObject<WifiNetDevice>();

请不要使用operator new创建这样的对象;而是使用CreateObject()来创建它们。

对于从SimpleRefCount类派生的对象,或其他支持使用智能指针类的对象,可以使用模板化的辅助函数:

Ptr<B> b = Create<B>();

这只是对正确处理引用计数系统的new操作符的包装。

总之,如果B不是对象,而只是使用引用计数(例如Packet),则使用Create<B>,如果B派生自ns3:: object,则使用CreateObject<B>

2.3.5 Aggregation

ns-3对象聚合系统在很大程度上是由于认识到ns-2的一个常见用例是使用继承和多态性来扩展协议模型。例如,TCP的特殊版本(如RenoTcpAgent)派生于TcpAgent类(并覆盖其函数)。

然而,在ns-2模型中出现了两个问题,即向下投射和“弱基类”。向下转换是指使用基类指针指向对象,并在运行时查询它以查找类型信息的过程,用于显式地将指针转换为子类指针,以便可以使用子类API。弱基类指的是由于缺乏必要的功能而导致类无法有效重用(从其派生)时出现的问题,导致开发人员不得不修改基类并导致基类API调用激增,其中一些调用可能在语义上不适合所有子类。

Ns-3使用查询接口设计模式的一个版本来避免这些问题。该设计基于Component Object ModelGNOME Bonobo的元素,尽管不支持可替换组件的完全二进制级兼容性,但我们已经尝试简化语法和对模型开发人员的影响。

2.3.6 Examples

Aggregation example

Node是在ns-3中使用聚合的一个很好的例子。请注意,在ns-3中没有像InternetNode这样的节点派生类。相反,组件(协议)被聚合到一个节点。让我们看看一些Ipv4协议是如何添加到节点的。

static void
AddIpv4Stack(Ptr<Node> node)
{
    Ptr<Ipv4L3Protocol> ipv4 = CreateObject<Ipv4L3Protocol>();
    ipv4->SetNode(node);
    node->AggregateObject(ipv4);
    Ptr<Ipv4Impl> ipv4Impl = CreateObject<Ipv4Impl>();
    ipv4Impl->SetIpv4(ipv4);
    node->AggregateObject(ipv4Impl);
}

注意,Ipv4协议是使用CreateObject()创建的。然后,它们被聚合到节点上。通过这种方式,不需要编辑Node基类来允许用户使用基类Node指针访问Ipv4接口;用户可以在运行时要求节点提供指向其Ipv4接口的指针。下一小节将描述用户请求节点的方式。

注意,将多个相同类型的对象聚合到ns3::Object是一个编程错误。因此,比如说,聚合并不是存储节点所有活动套接字的选择。

GetObject example

GetObject是一种类型安全的方法,用于实现安全的下转换,并允许在对象上找到接口。

考虑一个节点指针m_node,它指向一个node对象,该对象具有先前聚合到它的IPv4实现。客户端代码希望配置一个默认路由。为此,它必须访问节点内具有IP转发配置接口的对象。它执行以下操作:

Ptr<Ipv4> ipv4 = m_node->GetObject<Ipv4>();

如果该节点实际上没有聚合到它的Ipv4对象,那么该方法将返回null。因此,检查这样一个函数调用的返回值是一个很好的做法。如果成功,用户现在可以使用先前聚合到该节点的Ipv4对象的Ptr。

如何使用聚合的另一个示例是向对象添加可选模型。例如,一个现有的Node对象可以在运行时聚合一个“Energy Model”对象(无需修改和重新编译节点类)。现有模型(例如无线网络设备)可以稍后为能量模型“GetObject”,并在接口内置于底层Node对象或在运行时聚合到底层Node对象时适当地执行操作。然而,其他节点不需要知道能量模型。

我们希望这种编程模式将大大减少开发人员修改基类的需要。

2.3.7 Object factories

一个常见的用例是创建大量类似配置的对象。可以反复调用CreateObject(),但ns-3系统中也使用了工厂设计模式。它在“helper”API中被大量使用。

ObjectFactory可以用来实例化对象并配置这些对象的属性:

void SetTypeId(TypeId tid);
void Set(std::string name, const AttributeValue &value);
Ptr<T> Create() const;

第一个函数允许使用ns-3 TypeId系统指定创建的对象类型。第二个允许用户设置要创建的对象的属性,第三个允许用户创建对象本身。

例如:

ObjectFactory factory;
// 让这个工厂创建类型为 FriisPropagationLossModel的对象
factory.SetTypeId("ns3::FriisPropagationLossModel")
// 使此工厂对象更改属性的默认值,用于随后创建的对象
factory.Set("SystemLoss", DoubleValue(2.0));
// 创建一个这样的对象
Ptr<Object> object = factory.Create();
factory.Set("SystemLoss", DoubleValue(3.0));
// 用不同的SystemLoss创建另一个对象
Ptr<Object> object = factory.Create();

2.3.8 Downcasting

一个已经出现了几次的问题是,“如果我有一个基类指针(Ptr)到一个对象,我想要派生类指针,我应该向下强制转换(通过C++动态强制转换)来获得派生指针,或者我应该使用对象聚合系统GetObject<>()来找到一个Ptr到子类API的接口?”

答案是,在许多情况下,这两种技术都有效。ns-3提供了一个模板化函数,使对象动态强制转换的语法更加用户友好:

template <typename T1, typename T2>
Ptr<T1>
DynamicCast(Ptr<T2> const&p)
{
    return Ptr<T1>(dynamic_cast<T1 *>(PeekPointer(p)));
}

当程序员拥有基类型指针并针对子类指针进行测试时,DynamicCast可以工作。GetObject在查找聚合的不同对象时工作,但也与子类一起工作,与DynamicCast的方式相同。如果不确定,程序员应该使用GetObject,因为它在所有情况下都有效。如果程序员知道所考虑的对象的类层次结构,则直接使用DynamicCast更为直接。

2.4 Configuration and Attributes

在ns-3仿真中,配置有两个主要方面:

  • 仿真拓扑和对象如何连接。
  • 在拓扑中实例化的模型所使用的值。

本章着重于上面的第二项:在ns-3中使用的许多值是如何被用户进行组织、记录和修改的。ns-3属性系统也是在模拟器中收集跟踪和统计信息的基础。

在本章中,我们将讨论设置或修改ns-3模型对象使用的Value的各种方法。按专用性的递增顺序依次为:

方法范围
GetTypeId()中定义属性时设置的默认属性值。影响类的所有实例
CommandLine
Config::SetDefault()
ConfigStore
影响所有未来的实例。
ObjectFactory影响使用工厂创建的所有实例。
带有(string/ AttributeValue)参数对的助手方法影响助手创建的所有实例。
MyClass::SetX()
Object::SetAttribute()
Config::Set()
改变这个特定的实例。一般来说,这是唯一可以在模拟运行时安排更改实例的形式。

所谓“专用性”,我们指的是表中下行的方法覆盖由上行的方法设置的值,并且通常比上行的方法影响的实例更少。

在深入研究属性值系统的细节之前,回顾Object类的一些基本属性将会有所帮助。

2.4.1 Object Overview

ns-3基本上是一个基于C++对象的系统。我们的意思是新的C++类(类型)可以像往常一样被声明、定义和子类化。

许多ns-3对象继承自Object基类。这些对象有一些额外的属性,我们可以利用这些属性来组织系统和改进对象的内存管理:

  • “元数据”系统将类名链接到关于对象的大量元信息,包括:
    • 子类的基类,
    • 子类中可访问的构造函数集,
    • 子类的“属性”集,
    • 每个属性是否可以设置,或者是只读的,
    • 每个属性允许的值范围。
  • 引用计数智能指针实现,用于内存管理。

使用属性系统的ns-3对象派生自ObjectObjectBase。我们将要讨论的大多数ns-3对象都是从Object派生的,但是一些智能指针内存管理框架之外的对象是从ObjectBase派生的。

让我们回顾一下这些对象的几个属性。

Smart Pointers

正如ns-3教程中介绍的那样,ns-3对象是由引用计数智能指针实现Ptr类管理的内存。

智能指针在ns-3 API中广泛使用,以避免将引用传递给可能导致内存泄漏的堆分配对象。对于大多数基本用法(语法),将智能指针视为普通指针:

Ptr<WifiNetDevice> nd = ...;
nd->CallSomeFunction();
// etc.

那么怎样才能像本例中的第一行代码一样获得一个指向对象的智能指针呢?

CreateObject

正如我们在上面的[内存管理和类Ptr](#2.3.3 Memory management and class Ptr)中讨论的那样,在最低级别的API中,Object类型的对象不像通常那样使用operator new实例化,而是通过一个名为CreateObject()的模板化函数实例化。

创建这样一个对象的典型方法如下:

Ptr<WifiNetDevice> nd = CreateObject<WifiNetDevice>();

你可以认为这在功能上相当于:

WifiNetDevice* nd = new WifiNetDevice();

Object派生的对象必须使用CreateObject()在堆上分配。那些从ObjectBase派生的,比如ns-3助手函数和包头和包尾,可以在堆栈上分配。

在一些脚本中,您可能不会在代码中看到很多CreateObject()调用;实际上,这是因为有一些助手对象正在为您执行CreateObject()调用。

TypeId

从类Object派生的ns-3类可以包含一个称为TypeId的元数据类,它记录有关类的元信息,用于对象聚合和组件管理系统:

  • 标识类的唯一字符串。

  • 元数据系统中子类的基类。

  • 子类中可访问的构造函数集合。

  • 类的可公开访问的属性列表。

Object Summary

把所有这些概念放在一起,让我们看一个特定的示例:类Node

公共头文件node.h有一个包含静态GetTypeId()函数调用的声明:

class Node : public Object
{
public:
	static TypeId GetTypeId();
...

这在node.cc文件中的定义如下:

TypeId
Node::GetTypeId()
{
    static TypeId tid = TypeId("ns3::Node")
        .SetParent<Object>()
        .SetGroupName("Network")
        .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, // allow only getting it.
                      UintegerValue(0),
                      MakeUintegerAccessor(&Node::m_id),
                      MakeUintegerChecker<uint32_t>())
        ;
    return tid;
}

将ns-3 Object类的TypeId视为运行时类型信息(RTTI, run time type information)的扩展形式。C++语言包括一种简单的RTTI,以支持dynamic_casttypeid操作符。

上面定义中的SetParent<Object>()调用与我们的对象聚合机制一起使用,以允许在GetObject()期间在继承树中安全向上和向下转换。它还允许子类继承父类的属性。

AddConstructor<Node>()调用与抽象对象工厂机制一起使用,允许我们构造C++对象,而无需强制用户知道她正在构建的对象的具体类。

AddAttribute()的三个调用将给定字符串与类中的强类型值关联起来。请注意,您必须提供一个帮助字符串,例如,它可以通过命令行处理器显示。每个Attribute都与访问对象中底层成员变量的机制相关联(例如,MakeUintegerAccessor()告诉通用Attribute代码如何获得上面的节点ID)。还有“检查器”方法,用于根据范围限制验证值,例如允许的最大值和最小值。

当用户想要创建节点时,他们通常会调用某种形式的CreateObject(),:

Ptr<Node> n = CreateObject<Node>();

或者更抽象地说,使用对象工厂,你可以创建一个Node对象,甚至不知道具体的C++类型:

ObjectFactory factory;
const std::string typeId = "ns3::Node'';
factory.SetTypeId(typeId);
Ptr<Object> node = factory.Create <Object>();

这两种方法都会导致在生成的Object实例中可以使用完全初始化的属性。

接下来讨论如何将属性(与类的成员变量或函数关联的值)定位到上述TypeId

2.4.2 Attributes

属性系统的目标是组织对模拟内部成员对象的访问。这个目标的出现是因为,通常在模拟中,用户将剪切和粘贴/修改现有的模拟脚本,或者将使用更高级别的模拟结构,但通常会对研究或跟踪特定的内部变量感兴趣。例如,用例如下:

  • “我想仅在第一个接入点上跟踪无线接口上的数据包。
  • “我想在特定TCP套接字上跟踪TCP拥塞窗口的值(每次它发生变化时)。”
  • “我想要一个在我的模拟中使用的所有值的转储。”

类似地,用户可能希望对模拟中的内部变量进行细粒度访问,或者可能希望在所有随后创建的对象中广泛更改用于特定参数的初始值。最后,用户可能希望知道在模拟配置中哪些变量是可设置和可检索的。这不仅适用于命令行上的直接模拟交互;还要考虑一个(未来的)图形用户界面,它希望能够提供这样一种特性:用户可以右键单击画布上的节点,并看到一个分层的、有组织的参数列表,这些参数可以在节点及其组成成员对象上设置,以及每个参数的帮助文本和默认值。

Available AttributeValue Types
  • AddressValue

  • AttributeContainerValue

  • BooleanValue

  • BoxValue

  • CallbackValue

  • DataRateValue

  • DoubleValue

  • EmptyAttributeValue

  • EnumValue

  • IntegerValue

  • Ipv4AddressValue

  • Ipv4MaskValue

  • Ipv6AddressValue

  • Ipv6PrefixValue

  • LengthValue

  • Mac16AddressValue

  • Mac48AddressValue

  • Mac64AddressValue

  • ObjectFactoryValue

  • ObjectPtrContainerValue

  • PairValue<A, B>

  • PointerValue

  • PriomapValue

  • QueueSizeValue

  • RectangleValue

  • SsidValue

  • TimeValue

  • TupleValue<Args. . . >

    TupleValue能够存储不同类型的值,因此它适用于结构化数据。一个突出的例子是wifi的ChannelSettings属性,它由信道号、信道宽度、PHY频带和主20 MHz信道索引组成。在这种情况下,这些值必须相互一致,这使得很难将它们设置为单独的属性。在TupleValue中捕获它们可以简化这个问题,参见src/wifi/model/wifi-phy.cc

    存储在TupleValue对象中的值可以通过std::tuple对象设置/获取,或者可以序列化/反序列化来包含一对花括号中以逗号分隔的值序列的字符串(例如,“{36,20,BAND_5GHZ, 0}”)。

    TupleValue属性的用法见src/core/test/tuple-value-test-suite.cc

  • TypeIdValue

  • UanModesListValue

  • UintegerValue

  • Vector2DValue

  • Vector3DValue

  • WaypointValue

  • WifiModeValue

Defining Attributes

我们为用户提供了一种访问系统深处值的方法,而不必在系统中遍历访问器(指针)并遍历指针链来访问它们。考虑一个QueueBase类,它有一个成员变量m_maxSize来控制队列的深度。

如果我们看一下QueueBase的声明,可以看到以下内容:

class QueueBase : public Object {
public:
    static TypeId GetTypeId();
    ...
private:
    ...
    QueueSize m_maxSize; //!< 最大队列大小
    ...
};

QueueSize是ns-3中的一个特殊类型,它允许用不同的单位表示大小:

enum QueueSizeUnit
{
    PACKETS, /**< 为队列大小使用包数 */
    BYTES, /**< 为队列大小使用字节数 */
};
class QueueSize
{
	...
private:
    ...
    QueueSizeUnit m_unit; //!< 单位
    uint32_t m_value; //!< 队列大小[字节或包]
};

最后,DropTailQueue类继承了这个基类,并提供了提交到完整队列的数据包将从队列后面被丢弃的语义(“丢弃尾部”)。

/**
* \ingroup queue
*
* \brief 一种先进先出数据包队列,在溢出时丢弃尾部数据包
*/
template <typename Item>
class DropTailQueue : public Queue<Item>

让我们考虑一下用户可能想要对m_maxSize的值做的事情:

  • 为系统设置一个默认值,这样每当创建一个新的DropTailQueue时,这个成员被初始化为默认值。
  • 设置或获取一个已经实例化的队列的值。

上述功能通常需要提供Set()Get()函数,以及某种类型的全局默认值。

在ns-3属性系统中,这些值定义和访问器函数注册被移动到TypeId类中;例如:

NS_OBJECT_ENSURE_REGISTERED(QueueBase);
TypeId
QueueBase::GetTypeId()
{
    static TypeId tid = TypeId("ns3::DropTailQueue")
        .SetParent<Queue>()
        .SetGroupName("Network")
        ...
        .AddAttribute("MaxSize",
                      "The max queue size",
                      QueueSizeValue(QueueSize("100p")),
                      MakeQueueSizeAccessor(&QueueBase::SetMaxSize,
                                            &QueueBase::GetMaxSize),
                      MakeQueueSizeChecker())
        ...
        ;
    return tid;
}

Adddattribute()方法正在为m_maxSize值执行许多事情:

  • 绑定(通常是私有的)成员变量m_maxSize到一个公共字符串“MaxSize”。

  • 提供默认值(0个包)。

  • 提供一些帮助文本来定义值的含义。

  • 提供一个“检查器”(在这个例子中没有使用),可以用来设置值的允许范围的界限。

关键的一点是,现在这个变量的值及其默认值可以在属性名称空间中访问,该名称空间基于诸如“MaxSize”和TypeId名称字符串之类的字符串。在下一节中,我们将提供一个示例脚本,演示用户如何操作这些值。

注意,属性的初始化依赖于被调用的宏NS_OBJECT_ENSURE_REGISTERED(QueueBase);如果您在新类实现中忽略了这一点,则您的属性将无法正确初始化。

虽然我们已经描述了如何创建属性,但我们仍然没有描述如何访问和管理这些值。

例如,没有存放这些文件的globals.h头文件;属性与它们的类一起存储。自然出现的问题是,用户如何轻松地了解其模型的所有属性,以及用户如何访问这些属性,或将其值记录为其模拟记录的一部分?

API文档中提供了为类型定义的实际属性的详细文档,以及所有已定义属性的全局列表。在本文的其余部分,我们将演示获取和设置属性值的各种方法。

Setting Default Values
Config::SetDefault and CommandLine

让我们看看用户脚本如何访问特定的属性值。我们将使用src/point-to-point/ examples/main-attribute-value.cc脚本进行说明,并去掉一些细节。main函数如下:

// 这是一个使用属性系统在底层系统中设置和获取值的基本示例;
// 即设置和获取PointToPointNetDevice中FIFO队列的最大值的大小
//
int
main(int argc, char *argv[])
{
	// ns-3中的队列是在队列结构中包含项目(其他对象)的对象。
    // C++实现使用模板允许队列保存各种类型的项,
    // 但最常见的是指向数据包的指针(Ptr<packet>)。

    // 最大队列大小可以用字节('b')或数据包('p')强制执行。
    // 一种称为ns3::QueueSize的特殊类型可以以字节或数据包为
    // 单位保存队列大小值。队列基类ns3::QueueBase具有可以设置
    // 为QueueSize的MaxSize属性。默认情况下,MaxSize属性的值
    // 为100个数据包(“100p”)(此默认值可以在函数QueueBase::GetTypeId中观察到)

	// 在这里,我们将其设置为80个数据包。我们可以使用两种值类型之一
    // :基于字符串的值或QueueSizeValue值
    Config::SetDefault("ns3::QueueBase::MaxSize", StringValue("80p"));
    // 以下函数调用是多余的
    Config::SetDefault("ns3::QueueBase::MaxSize", QueueSizeValue(QueueSize(QueueSizeUnit::PACKETS, 80)));

上面要注意的主要事情是对Config::SetDefault()的两个等效调用。这就是我们为所有随后实例化的DropTailQueues设置默认值的方式。我们举例说明了两种类型的Value类,StringValueQueueSizeValue类,可用于将值赋给命名为“ns3::QueueBase::MaxSize”的属性。

也可以使用CommandLine来操作属性;我们在ns-3教程中看到了一些例子。特别是,对于与您的模型特别相关的属性(在本例中为"ns3::QueueBase:: maxSize"),添加一个简写的参数名称是很简单的,例如--maxSize。这还有一个额外的特性,即Attribute的帮助字符串将作为脚本使用消息的一部分打印出来。有关更多信息,请参阅CommandLine API文档。

// Allow the user to override any of the defaults and the above
// SetDefaults() at run-time, via command-line arguments
// For example, via "--ns3::QueueBase::MaxSize=80p"
CommandLine cmd;
// This provides yet another way to set the value from the command line:
cmd.AddValue("maxSize", "ns3::QueueBase::MaxSize");
cmd.Parse(argc, argv);

现在,我们将使用低级API创建一些对象。我们新创建的队列将不会像QueueBase::GetTypeId()函数中定义的那样,将m_maxSize初始化为0个数据包,而是初始化为80个数据包,因为我们上面使用了默认值。

Ptr<Node> n0 = CreateObject<Node>();
Ptr<PointToPointNetDevice> net0 = CreateObject<PointToPointNetDevice>();
n0->AddDevice(net0);
Ptr<Queue<Packet>> q = CreateObject<DropTailQueue<Packet>> ();
net0->AddQueue(q);

此时,我们已经创建了单个Node (n0)和单个PointToPointNetDevice (net0),并向net0添加了DropTailQueue (q),它将被配置为队列大小限制为80个数据包。

最后要说明的是,Config::Set…()函数将抛出一个错误,如果目标属性在给定的路径中不存在。当然也有“fail-safe”版本,如果不能确定该属性是否存在,则使用Config::Set…FailSafe()。如果至少可以设置一个实例,则fail-safe版本返回true。

Constructors, Helpers and ObjectFactory

属性的任意组合可以从助手和低级API中设置和获取;或者来自构造函数本身:

Ptr<GridPositionAllocator> p =
    CreateObjectWithAttributes<GridPositionAllocator>
        ("MinX", DoubleValue(-100.0),
         "MinY", DoubleValue(-100.0),
         "DeltaX", DoubleValue(5.0),
         "DeltaY", DoubleValue(20.0),
         "GridWidth", UintegerValue(20),
         "LayoutType", StringValue("RowFirst"));

或来自更高级的助手api,例如:

mobility.SetPositionAllocator
    ("ns3::GridPositionAllocator",
     "MinX", DoubleValue(-100.0),
     "MinY", DoubleValue(-100.0),
     "DeltaX", DoubleValue(5.0),
     "DeltaY", DoubleValue(20.0),
     "GridWidth", UintegerValue(20),
     "LayoutType", StringValue("RowFirst"));

我们在这里不进行说明,但是您也可以为特定属性配置ObjectFactory,并为其设置新值。由ObjectFactory创建的实例将在构造期间设置这些属性。这与为类使用一个助手api非常相似。

回顾一下,有几种方法可以为将来创建的类实例设置属性值:

  • Config::SetDefault()
  • CommandLine::AddValue()
  • CreateObjectWithAttributes<>()
  • 各种助手API

但是,如果您已经创建了一个实例,并且希望更改该属性的值,该怎么办呢?在这个例子中,我们如何操作已经实例化的DropTailQueuem_maxSize值?这里有很多方法可以做到这一点。

Changing Values
SmartPointer

假设手头有一个指向相关网络设备的智能指针(Ptr);在当前的示例中,它是net0指针。

更改该值的一种方法是访问指向底层队列的指针并修改其属性。

首先,我们观察到我们可以通过PointToPointNetDevice属性获得一个指向Queue(基类)的指针,它被称为“TxQueue”:

PointerValue ptr;
net0->GetAttribute("TxQueue", ptr);
Ptr<Queue<Packet>> txQueue = ptr.Get<Queue<Packet>>();

使用GetObject()函数,我们可以对DropTailQueue执行安全的下转换。NS_ASSERT检查指针是否有效

Ptr<DropTailQueue<Packet>> dtq = txQueue->GetObject<DropTailQueue<Packet>>();
NS_ASSERT (dtq);

接下来,我们可以获取该队列上的属性值。我们为底层数据类型引入了包装器Value类,类似于围绕这些类型的Java包装器,因为属性系统存储序列化为字符串的值,而不是不同的类型。这里,此值的属性值方法产生(未包装的)QueueSize。也就是说,它被分配给一个QueueSizeValue,并且Get()的变量限制被GetAttribute方法写入。

QueueSizeValue limit;
dtq->GetAttribute("MaxSize", limit);
NS_LOG_INFO("1. dtq limit: " << limit.Get());

请注意,上面的悲观预测并不是真正需要的;我们可以直接从txQueue中获得属性值:

txQueue->GetAttribute("MaxSize", limit);
NS_LOG_INFO("2. txQueue limit: " << limit.Get());

现在,让我们将其设置为另一个值(60个数据包)。我们还可以使用StringValue速记表示法通过传入一个字符串来设置大小(字符串必须是一个以p或b字符结尾的正整数)。

txQueue->SetAttribute("MaxSize", StringValue("60p"));
txQueue->GetAttribute("MaxSize", limit);
NS_LOG_INFO("3. txQueue limit changed: " << limit.Get());
Config Namespace Path

获取该属性的另一种方法是使用配置名称空间。在这里,这个属性驻留在这个命名空间中的一个已知路径上;如果无法访问底层指针,并且希望使用单个语句配置特定属性,则此方法非常有用。

Config::Set("/NodeList/0/DeviceList/0/TxQueue/MaxSize",
            StringValue("25p"));
txQueue->GetAttribute("MaxSize", limit);
NS_LOG_INFO("4. txQueue limit changed through namespace: "
            << limit.Get());

配置路径的形式通常是".../<container name>/<index>/.../<attribute>/ <attribute>"通过容器中对象的索引来引用特定实例。在这种情况下,第一个容器是所有Nodes的列表;第二个容器是所选Node上所有Netdevices的列表。最后,配置路径通常以一系列成员属性结束,在本例中是所选NetDevice的“TxQueue”的“MaxSize”属性。

我们也可以使用通配符为所有节点和所有网络设备设置这个值(在这个简单的例子中,它与前面的Config:: Set()具有相同的效果):

Config::Set("/NodeList/*/DeviceList/*/TxQueue/MaxSize",
            StringValue("15p"));
txQueue->GetAttribute("MaxSize", limit);
NS_LOG_INFO("5. txQueue limit changed through wildcarded namespace: "
            << limit.Get());

如果你从命令行运行这个程序,你应该看到下面的输出,对应于我们上面所做的步骤:

$ ./ns3 run main-attribute-value
1. dtq limit: 80p
2. txQueue limit: 80p
3. txQueue limit changed: 60p
4. txQueue limit changed through namespace: 25p
5. txQueue limit changed through wildcarded namespace: 15p
Object Name Service

获取该属性的另一种方法是使用对象名称服务功能。对象名称服务允许我们使用用户定义的名称字符串将项添加到“/Names/”路径下的配置名称空间中。如果无法访问底层指针,并且难以确定所需的具体配置名称空间路径,则此方法非常有用。

Names::Add("server", n0);
Names::Add("server/eth0", net0);
...
Config::Set("/Names/server/eth0/TxQueue/MaxPackets", UintegerValue(25));

在这里,我们在“/Names/”名称空间下添加了路径元素“server”和“eth0”,然后使用生成的配置路径来设置属性。

有关ns-3配置命名空间的更详细描述,请参阅[对象名称](#2.5 Object names )。

2.4.3 Implementation Details

Value Classes

读者会注意到TypeValue类是AttributeValue基类的子类。这些可以看作是中间类,用于将原始类型转换为属性系统使用的AttributeValues。回想一下,这个数据库保存了许多类型的对象,这些对象被序列化为字符串。这种类型的转换可以使用中间类(如IntegerValue,或浮点数的DoubleValue)或通过字符串完成。直接隐式地将类型转换为AttributeValue并不实际。所以在上面,用户可以选择使用字符串或值:

p->Set("cwnd", StringValue("100")); // string-based setter
p->Set("cwnd", IntegerValue(100)); // integer-based setter

系统提供了一些宏来帮助用户为他们想要引入属性系统的新类型声明和定义新的AttributeValue子类:

  • ATTRIBUTE_HELPER_HEADER
  • ATTRIBUTE_HELPER_CPP

有关这些构造的更多信息,请参阅API文档。

Initialization Order

系统中的属性不能依赖于系统中任何其他属性的状态。这是因为系统没有指定或强制执行属性初始化的顺序。一个具体的例子可以在自动配置程序中看到,比如ConfigStore。尽管给定的模型可以安排它,以便按照特定的顺序初始化属性,但另一个自动配置器可以独立地决定按照例如字母顺序来更改属性。

由于这种非特定的排序,系统中的任何属性都不可能依赖于任何其他属性。作为推论,属性设置器绝不能因为另一个属性的状态而失败。任何属性设置器都不能因为改变属性值而改变(设置)任何其他属性值。

这是一个非常强的限制,在某些情况下,必须一致地设置Attributes以允许正确的操作。为此,我们允许在使用属性时进行一致性检查(参见NS_ASSERT_MSGNS_ABORT_MSG)。

通常,在构造对象之后执行将值赋给底层类成员变量的属性代码。但是,如果需要在构造函数体执行之前赋值,因为需要在构造函数的逻辑中赋值,该怎么办呢?有一种方法可以做到这一点,例如在类ConfigStore:调用ObjectBase::ConstructSelf()中使用,如下所示:

ConfigStore::ConfigStore()
{
    ObjectBase::ConstructSelf(AttributeConstructionList());
    // continue on with constructor.
}

注意,对象及其所有派生类也必须实现GetInstanceTypeId()方法。否则ObjectBase::ConstructSelf()将无法读取属性。

Adding Attributes

ns-3系统将在属性系统下放置许多内部值,但毫无疑问,用户将希望扩展它以选择我们错过的值,或者将自己的类添加到系统中。

有三种典型的用例:

  • 使现有的类数据成员作为属性可访问,当它还没有。

  • 让一个新类能够通过给它一个TypeId来公开一些数据成员作为属性。

  • 为新类创建AttributeValue子类,以便它可以作为Attribute访问。

Existing Member Variable

考虑TcpSocket中的这个变量:

uint32_t m_cWnd; // Congestion window

假设使用TCP的人员希望使用元数据系统获取或设置该变量的值。如果ns-3还没有提供,用户可以在运行时元数据系统中声明以下添加(到TcpSocketGetTypeId()定义中):

.AddAttribute("Congestion window",
              "Tcp congestion window(bytes)",
              UintegerValue(1),
              MakeUintegerAccessor(&TcpSocket::m_cWnd),
              MakeUintegerChecker<uint16_t>())

现在,拥有指向TcpSocket实例的指针的用户可以执行诸如设置和获取值之类的操作,而不必显式地添加这些函数。此外,还可以应用访问控制,例如允许读取而不写入参数,或者可以应用允许值的边界检查。

New Class TypeId

这里,我们将讨论想要向ns-3添加新类的用户所受到的影响。必须做哪些额外的事情来使它能够保存属性?

假设我们的新类ns3::MyMobility是一种移动性模型。首先,这个类应该继承它的父类ns3::MobilityModel。在my- mobile .h头文件中:

namespace ns3 {
    class MyMobility : public MobilityModel
    {

这需要声明GetTypeId()函数。这是一个一行的公共函数声明:

public:
    /**
     * Register this type.
     * \return The object TypeId.
     */
    static TypeId GetTypeId();

我们已经在实现文件my-mobility.cc中介绍了TypeId的定义:

NS_OBJECT_ENSURE_REGISTERED(MyMobility);
TypeId
    MyMobility::GetTypeId()
{
    static TypeId tid = TypeId("ns3::MyMobility")
        .SetParent<MobilityModel>()
        .SetGroupName("Mobility")
        .AddConstructor<MyMobility>()
        .AddAttribute("Bounds",
                      "Bounds of the area to cruise.",
                      RectangleValue(Rectangle(0.0, 0.0, 100.0, 100.0)),
                      MakeRectangleAccessor(&MyMobility::m_bounds),
                      MakeRectangleChecker())
        .AddAttribute("Time",
                      "Change current direction and speed after moving for this delay.",
                      // etc (more parameters).
                      TimeValue(Seconds(1.0)),
                      MakeTimeAccessor(&MyMobility::m_modeTime),
                      MakeTimeChecker())
        ;
    return tid;
}

如果我们不想从现有的类中继承子类,在头文件中我们只需要继承ns3::Object,在Object文件中我们使用. SetParent<Object> ()将父类设置为ns3::Object

这里的典型错误包括:

  • 没有调用NS_OBJECT_ENSURE_REGISTERED()

  • 没有调用SetParent()方法,或者用错误的类型调用它。

  • 没有调用AddConstructor()方法,或者用错误的类型调用它。

  • 在构造函数中TypeId的名称中引入了一个印刷错误。

  • 没有使用封闭的C++类的完全限定的C++类型名作为TypeId的名称。注意"ns3::"是必需的。

这些错误都不能被ns-3代码库检测到,因此建议用户仔细检查多次,以确保这些错误是正确的。

New AttributeValue Type

从在系统中编写新类并希望将其作为属性访问的用户的角度来看,主要是编写与字符串和属性值之间的转换。其中大部分可以通过宏化代码复制/粘贴。例如,考虑src/mobility/model目录中Rectangle的类声明:

Header File
/**
 * \brief a 2d rectangle
 */
class Rectangle
{
    ...
    double xMin;
    double xMax;
    double yMin;
    double yMax;
};

必须在类声明下面添加一个宏调用和两个操作符,以便将Rectangle转换为Attribute系统可用的值:

std::ostream &operator <<(std::ostream &os, const Rectangle &rectangle);
std::istream &operator >>(std::istream &is, Rectangle &rectangle);
ATTRIBUTE_HELPER_HEADER(Rectangle);
Implementation File

在类定义中(.cc文件),代码看起来像这样:

ATTRIBUTE_HELPER_CPP(Rectangle);
std::ostream &
    operator <<(std::ostream &os, const Rectangle &rectangle)
{
    os << rectangle.xMin << "|" << rectangle.xMax << "|" << rectangle.yMin << "|"
       << rectangle.yMax;
    return os;
}
std::istream &
operator >>(std::istream &is, Rectangle &rectangle)
{
    char c1, c2, c3;
    is >> rectangle.xMin >> c1 >> rectangle.xMax >> c2 >> rectangle.yMin >> c3
       >> rectangle.yMax;
    if (c1 != '|' ||
        c2 != '|' ||
        c3 != '|')
        {
            is.setstate(std::ios_base::failbit);
        }
    return is;
}

这些流操作符简单地将矩形的字符串表示形式(“xMin|xMax|yMin|yMax”)转换为底层矩形。建模者必须指定这些操作符和新类实例的字符串语法表示形式。

2.4.4 ConfigStore

ns-3属性的值可以存储在ASCII或XML文本文件中,并加载到将来的模拟运行中。这个特性被称为ns-3 ConfigStore。ConfigStore是一个专门用于属性值和默认值的数据库。

虽然它是src/config-store/目录中单独维护的模块,但我们在这里记录它,因为它只依赖于ns-3核心模块和属性。

我们可以使用src/config-store/examples/config-store-save.cc中的一个例子来探索这个系统。

首先,ConfigStore的所有用户必须包含以下语句:

#include "ns3/config-store-module.h"

接下来,这个程序添加一个示例对象ConfigExample来显示系统是如何扩展的:

class ConfigExample : public Object
{
    public:
    static TypeId GetTypeId() {
        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);

接下来,我们使用Config子系统以几种方式覆盖默认设置:

Config::SetDefault("ns3::ConfigExample::TestInt16", IntegerValue(-5));
Ptr<ConfigExample> a_obj = CreateObject<ConfigExample>();
NS_ABORT_MSG_UNLESS(a_obj->m_int16 == -5,
                    "Cannot set ConfigExample's integer attribute via
                    ,!Config::SetDefault");
                    Ptr<ConfigExample> a2_obj = CreateObject<ConfigExample>();
                    a2_obj->SetAttribute("TestInt16", IntegerValue(-3));
                    IntegerValue iv;
                    a2_obj->GetAttribute("TestInt16", iv);
                    NS_ABORT_MSG_UNLESS(iv.Get() == -3,
                                        "Cannot set ConfigExample's integer attribute via SetAttribute");

下一个语句是必要的,以确保创建的(其中一个)对象作为对象实例扎根于配置名称空间中。这通常发生在你将对象聚合到ns3::Nodens3::Channel实例时,但在这里,由于我们在核心级别工作,我们需要创建一个新的根命名空间对象:

Config::RegisterRootNamespaceObject(a2_obj);
Writing

接下来,我们要输出配置存储。示例展示了如何以两种格式(XML和原始文本)完成此操作。在实践中,应该在调用Simulator::Run()之前执行此步骤,以便在运行模拟之前保存最终配置。

有三个属性控制ConfigStore的行为:“Mode”、“Filename”和“FileFormat”。

Mode(默认为"None")配置ns-3是应该从先前保存的文件中加载配置(指定"Mode=Load")还是将其保存到文件中(指定"Mode=Save")。Filename(默认为"")是ConfigStore应该读取或写入其数据的位置。FileFormat(默认为“RawText”)决定ConfigStore格式是纯文本还是Xml(“FileFormat=Xml”)。示例如下:

Config::SetDefault("ns3::ConfigStore::Filename", StringValue("output-attributes.xml"));
Config::SetDefault("ns3::ConfigStore::FileFormat", StringValue("Xml"));
Config::SetDefault("ns3::ConfigStore::Mode", StringValue("Save"));
ConfigStore outputConfig;
outputConfig.ConfigureDefaults();
outputConfig.ConfigureAttributes();
// Output config store to txt format
Config::SetDefault("ns3::ConfigStore::Filename", StringValue("output-attributes.txt"));
Config::SetDefault("ns3::ConfigStore::FileFormat", StringValue("RawText"));
Config::SetDefault("ns3::ConfigStore::Mode", StringValue("Save"));
ConfigStore outputConfig2;
outputConfig2.ConfigureDefaults();
outputConfig2.ConfigureAttributes();
Simulator::Run();
Simulator::Destroy();

请注意,这些语句的位置正好在开始模拟之前(即在所有配置完成之后)的值之前。

运行后,你可以打开output-attributes.txt文件,看到:

...
default ns3::ErrorModel::IsEnabled "true"
default ns3::RateErrorModel::ErrorUnit "ERROR_UNIT_BYTE"
default ns3::RateErrorModel::ErrorRate "0"
default ns3::RateErrorModel::RanVar "ns3::UniformRandomVariable[Min=0.0|Max=1.0]"
default ns3::BurstErrorModel::ErrorRate "0"
default ns3::BurstErrorModel::BurstStart "ns3::UniformRandomVariable[Min=0.0|Max=1.0]"
default ns3::BurstErrorModel::BurstSize "ns3::UniformRandomVariable[Min=1|Max=4]"
default ns3::PacketSocket::RcvBufSize "131072"
default ns3::PcapFileWrapper::CaptureSize "65535"
default ns3::PcapFileWrapper::NanosecMode "false"
default ns3::SimpleNetDevice::PointToPointMode "false"
default ns3::SimpleNetDevice::TxQueue "ns3::DropTailQueue<Packet>"
default ns3::SimpleNetDevice::DataRate "0bps"
default ns3::PacketSocketClient::MaxPackets "100"
default ns3::PacketSocketClient::Interval "+1000000000.0ns"
default ns3::PacketSocketClient::PacketSize "1024"
default ns3::PacketSocketClient::Priority "0"
default ns3::ConfigStore::Mode "Save"
default ns3::ConfigStore::Filename "output-attributes.txt"
default ns3::ConfigStore::FileFormat "RawText"
default ns3::ConfigExample::TestInt16 "-5"
global SimulatorImplementationType "ns3::DefaultSimulatorImpl"
global SchedulerType "ns3::MapScheduler"
global RngSeed "1"
global RngRun "1"
global ChecksumEnabled "false"
value /$ns3::ConfigExample/TestInt16 "-3"

在上面,显示了核心模块和网络模块属性的几个默认值。然后,记录ns-3全局值的所有值。最后,显示了根植于配置命名空间的ConfigExample实例的值。在真正的ns-3程序中,将显示更多的模型、属性和默认值。

output-attributes.xml中也存在XML版本:

<?xml version="1.0" encoding="UTF-8"?>
<ns3>
    <default name="ns3::ErrorModel::IsEnabled" value="true"/>
    <default name="ns3::RateErrorModel::ErrorUnit" value="ERROR_UNIT_BYTE"/>
    <default name="ns3::RateErrorModel::ErrorRate" value="0"/>
    <default name="ns3::RateErrorModel::RanVar" value="ns3::UniformRandomVariable[Min=0.0|Max=1.0]"/>
    <default name="ns3::BurstErrorModel::ErrorRate" value="0"/>
    <default name="ns3::BurstErrorModel::BurstStart" value="ns3::UniformRandomVariable[Min=0.0|Max=1.0]"/>
    <default name="ns3::BurstErrorModel::BurstSize" value="ns3::UniformRandomVariable[Min=1|Max=4]"/>
    <default name="ns3::PacketSocket::RcvBufSize" value="131072"/>
    <default name="ns3::PcapFileWrapper::CaptureSize" value="65535"/>
    <default name="ns3::PcapFileWrapper::NanosecMode" value="false"/>
    <default name="ns3::SimpleNetDevice::PointToPointMode" value="false"/>
    <default name="ns3::SimpleNetDevice::TxQueue" value="ns3::DropTailQueue&lt;Packet&gt;"/>
    <default name="ns3::SimpleNetDevice::DataRate" value="0bps"/>
    <default name="ns3::PacketSocketClient::MaxPackets" value="100"/>
    <default name="ns3::PacketSocketClient::Interval" value="+1000000000.0ns"/>
    <default name="ns3::PacketSocketClient::PacketSize" value="1024"/>
    <default name="ns3::PacketSocketClient::Priority" value="0"/>
    <default name="ns3::ConfigStore::Mode" value="Save"/>
    <default name="ns3::ConfigStore::Filename" value="output-attributes.xml"/>
    <default name="ns3::ConfigStore::FileFormat" value="Xml"/>
    <default name="ns3::ConfigExample::TestInt16" value="-5"/>
    <global name="SimulatorImplementationType" value="ns3::DefaultSimulatorImpl"/>
    <global name="SchedulerType" value="ns3::MapScheduler"/>
    <global name="RngSeed" value="1"/>
    <global name="RngRun" value="1"/>
    <global name="ChecksumEnabled" value="false"/>
    <value path="/$ns3::ConfigExample/TestInt16" value="-3"/>
</ns3>

该文件可以与模拟脚本和输出数据一起存档。

Reading

接下来,我们将讨论通过存储的输入配置文件配置模拟。与编写最终的模拟配置相比,有几个关键的区别。首先,在编写模拟配置语句之前,我们需要在程序的开头放置这样的语句(因此在对象构造中使用这些值之前注册这些值)。

Config::SetDefault("ns3::ConfigStore::Filename", StringValue("input-defaults.xml"));
Config::SetDefault("ns3::ConfigStore::Mode", StringValue("Load"));
Config::SetDefault("ns3::ConfigStore::FileFormat", StringValue("Xml"));
ConfigStore inputConfig;
inputConfig.ConfigureDefaults();

接下来,请注意,输入配置数据的加载仅限于Attribute默认值(即不是实例)和全局值。不支持属性实例值,因为在模拟的这个阶段,在构造任何对象之前,周围没有这样的对象实例。(注意,对配置存储的未来增强可能会改变此行为)。

其次,虽然ConfigStore状态的输出将列出数据库中的所有内容,但输入文件只需要包含要覆盖的特定值。因此,将该类用于输入文件配置的一种方法是使用输出(“Save”)生成初始配置“Mode”,从该配置文件中只提取希望更改的元素,并将这些最小元素移动到新的配置文件中,然后可以在随后的模拟运行中安全地编辑和加载。

ConfigStore对象实例化时,必须通过命令行或程序语句设置其属性“Filename”、“Mode”和“FileFormat”。

Reading/Writing Example

作为一个更复杂的例子,让我们假设我们想要从一个名为input-defaults.xml的输入文件中读取默认配置,并将结果属性写入一个名为output-attributes.xml的单独文件:

#include "ns3/config-store-module.h"
...
    int main(...)
{
    Config::SetDefault("ns3::ConfigStore::Filename", StringValue("input-defaults.xml"));
    Config::SetDefault("ns3::ConfigStore::Mode", StringValue("Load"));
    Config::SetDefault("ns3::ConfigStore::FileFormat", StringValue("Xml"));
    ConfigStore inputConfig;
    inputConfig.ConfigureDefaults();
    //
    // Allow the user to override any of the defaults and the above Bind() at
    // run-time, viacommand-line arguments
    //
    CommandLine cmd;
    cmd.Parse(argc, argv);
    // setup topology
    ...
        // Invoke just before entering Simulator::Run()
        Config::SetDefault("ns3::ConfigStore::Filename", StringValue("output-attributes.xml"));
    Config::SetDefault("ns3::ConfigStore::Mode", StringValue("Save"));
    ConfigStore outputConfig;
    outputConfig.ConfigureAttributes();
    Simulator::Run();
}
ConfigStore use cases (pre- and post-simulation)

值得强调的是,ConfigStore可以用于不同的目的,这反映在脚本中调用ConfigStore的位置。

典型的用例是:

  • 更改对象默认属性
  • 检查/更改特定对象属性
  • 检查模拟对象及其属性

事实上,一些对象可能在模拟开始时创建。因此,如果在代码的早期调用,ConfigStore将不会“报告”它们的属性。

一个典型的工作流程可能涉及运行模拟,在模拟结束时调用ConfigStore(在Simulator::Run()之后,在Simulator::Destroy()之前)。这将显示对象中的所有属性,包括那些具有默认值的属性,以及那些在模拟执行期间具有更改值的属性。

要更改这些值,您需要更改默认(类范围)属性值(在这种情况下,在对象创建之前调用ConfigStore)或特定对象属性(在这种情况下,在对象创建之后调用ConfigStore,通常在Simulator::Run()之前)。

ConfigStore GUI

ConfigStore有一个基于GTK的前端。这允许用户使用GUI访问和更改变量。

这里提供了一些屏幕截图。它们是在Simulator::Run()之后在src/lte/examples/ lena-dual-stripe.cc上使用GtkConfig的结果。

要使用这个特性,必须安装libgtk-3-dev;Ubuntu安装命令示例如下:

$ sudo apt-get install libgtk-3-dev

在MacOS上,可以使用Homebrew安装GTK-3。安装命令如下:

$ brew install gtk+3 adwaita-icon-theme

查看该步骤是否配置,请查看该步骤的输出:

$ ./ns3 configure --enable-examples --enable-tests
---- Summary of optional NS-3 features:
Python Bindings 			: enabled
Python API Scanning Support : enabled
NS-3 Click Integration 		: enabled
GtkConfigStore 				: not enabled (library 'gtk+-3.0 >= 3.0' not found)

在上面的示例中,没有启用它,因此在安装了合适的版本并且返回:

$ ./ns3 configure --enable-examples --enable-tests
$ ./ns3

之前不能使用它。

用法与非基于gtk的版本几乎相同,但没有涉及ConfigStore属性:

// 在进入模拟器之前调用Simulator::Run()
GtkConfigStore config;
config.ConfigureDefaults();
config.ConfigureAttributes();

现在,当您运行脚本时,应该弹出一个GUI,允许您打开不同节点/对象上的属性菜单,然后在完成后启动模拟执行。

注意,“启动模拟”意味着继续执行模拟脚本。如果GtkConfigStore在Simulator::Run()之后被调用,模拟将不会再次启动-它将刚刚结束。

2.5 Object names

章占位符(官方也没有写)
在这里插入图片描述
在这里插入图片描述

2.6 RealTime

Ns-3设计用于集成到测试平台和虚拟机环境中。为了与真实的网络堆栈集成并发出/消费数据包,需要一个实时调度器来尝试用硬件时钟锁定模拟时钟。我们在这里描述其中的一个组件: 实时调度器(RealTime scheduler)。

实时调度器的目的是使模拟时钟的进程相对于某些外部时间基同步发生。在没有外部时间基础(挂钟)的情况下,模拟时间从一个模拟时间瞬间跳到下一个模拟时间。

2.6.1 Behavior

当使用非实时调度器(ns-3中的默认值)时,模拟器将模拟时间提前到下一个计划的事件。在事件执行期间,模拟时间被冻结。使用实时调度器,从仿真模型的角度来看,其行为是相似的(即,仿真时间在事件执行期间被冻结),但是在事件之间,模拟器将尝试保持仿真时钟与机器时钟对齐。

当一个事件完成执行,调度器移动到下一个事件时,调度器将下一个事件的执行时间与机器时钟进行比较。如果下一个事件被安排在未来的某个时间,则模拟器将休眠,直到达到实时时间,然后执行下一个事件。

可能会发生这样的情况,由于执行模拟事件时固有的处理,模拟器无法跟上实时。在这种情况下,要做什么取决于用户配置。有两个ns-3属性控制该行为。第一个是ns3::RealTimeSimulatorImpl::SynchronizationMode。此属性可能的两个条目是BestEffort(默认值)或HardLimit。在“BestEffort”模式下,模拟器将通过执行事件来赶上实时,直到它到达下一个事件(实时)未来的点,否则模拟将结束。在BestEffort模式下,模拟可能会消耗比挂钟时间更多的时间。如果超出容忍阈值,另一个选项“HardLimit”将导致模拟中止。该属性为ns3::RealTimeSimulatorImpl::HardLimit,默认值为0.1秒。

另一种操作模式是在事件执行期间冻结模拟时间。这种实时模拟模式被实现了,但由于是否有用的问题,从ns-3树中删除了。如果用户对在事件执行期间模拟时间不冻结的实时模拟器感兴趣(即,每次调用simulator::Now()返回当前挂钟时间,而不是事件开始执行的时间),请联系ns-developers邮件列表。

2.6.2 Usage

从脚本的角度来看,实时模拟器的使用非常简单。用户只需要将属性SimulatorImplementationType设置为实时模拟器,如下所示:

GlobalValue::Bind("SimulatorImplementationType",
		           StringValue("ns3::RealtimeSimulatorImpl"));

examples/realtime/realtime-udp-echo.cc中有一个脚本,其中有一个如何配置实时行为的示例。试一试:

$ ./ns3 run realtime-udp-echo

模拟器将以最佳方式工作还是以硬限制策略方式工作是由前一节中解释的属性控制的。

2.6.3 Implementation

实现包含在以下文件中:

  • src/core/model/realtime-simulator-impl.{cc,h}
  • src/core/model/wall-clock-synchronizer.{cc,h}

为了创建一个实时调度器,在第一个近似中,你只需要引起模拟时间跳转来消耗实时时间。我们建议使用睡眠和忙碌等待相结合的方法来做到这一点。睡眠等待导致调用进程(线程)在一段时间内放弃处理器。尽管可以将指定的时间量转换为纳秒级分辨率,但它实际上被转换为特定于操作系统的粒度。在Linux中,这种粒度被称为瞬间(Jiffy)。通常情况下,这个分辨率不足以满足我们的需求(大约10毫秒),因此我们将其舍入并休眠一些较小的瞬间。然后在指定数量的瞬间通过后唤醒该进程。此时,我们还有一些剩余时间等待。这个时间通常比最小睡眠时间短,所以我们忙着等待剩余的时间。这意味着线程只是坐在for循环中消耗周期,直到所需的时间到达。在睡眠和忙碌等待组合之后,经过的实时时钟(墙)应该与下一个事件的模拟时间一致,然后继续进行模拟。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值